diff --git a/.coveragerc b/.coveragerc index 9f2fdc80716..2c768060108 100644 --- a/.coveragerc +++ b/.coveragerc @@ -29,6 +29,8 @@ omit = homeassistant/components/agent_dvr/camera.py homeassistant/components/agent_dvr/const.py homeassistant/components/agent_dvr/helpers.py + homeassistant/components/airnow/__init__.py + homeassistant/components/airnow/sensor.py homeassistant/components/airvisual/__init__.py homeassistant/components/airvisual/air_quality.py homeassistant/components/airvisual/sensor.py @@ -142,7 +144,7 @@ omit = homeassistant/components/co2signal/* homeassistant/components/coinbase/* homeassistant/components/comed_hourly_pricing/sensor.py - homeassistant/components/comfoconnect/* + homeassistant/components/comfoconnect/fan.py homeassistant/components/concord232/alarm_control_panel.py homeassistant/components/concord232/binary_sensor.py homeassistant/components/control4/__init__.py @@ -213,7 +215,11 @@ omit = homeassistant/components/ecobee/notify.py homeassistant/components/ecobee/sensor.py homeassistant/components/ecobee/weather.py - homeassistant/components/econet/* + homeassistant/components/econet/__init__.py + homeassistant/components/econet/binary_sensor.py + homeassistant/components/econet/const.py + homeassistant/components/econet/sensor.py + homeassistant/components/econet/water_heater.py homeassistant/components/ecovacs/* homeassistant/components/edl21/* homeassistant/components/eddystone_temperature/sensor.py @@ -298,8 +304,8 @@ omit = homeassistant/components/folder_watcher/* homeassistant/components/foobot/sensor.py homeassistant/components/fortios/device_tracker.py + homeassistant/components/foscam/__init__.py homeassistant/components/foscam/camera.py - homeassistant/components/foscam/const.py homeassistant/components/foursquare/* homeassistant/components/free_mobile/notify.py homeassistant/components/freebox/__init__.py @@ -308,6 +314,9 @@ omit = homeassistant/components/freebox/sensor.py homeassistant/components/freebox/switch.py homeassistant/components/fritz/device_tracker.py + homeassistant/components/fritzbox_callmonitor/__init__.py + homeassistant/components/fritzbox_callmonitor/const.py + homeassistant/components/fritzbox_callmonitor/base.py homeassistant/components/fritzbox_callmonitor/sensor.py homeassistant/components/fritzbox_netmonitor/sensor.py homeassistant/components/fronius/sensor.py @@ -356,7 +365,10 @@ omit = homeassistant/components/hangouts/hangouts_bot.py homeassistant/components/hangouts/hangups_utils.py homeassistant/components/harman_kardon_avr/media_player.py - homeassistant/components/harmony/* + homeassistant/components/harmony/const.py + homeassistant/components/harmony/data.py + homeassistant/components/harmony/remote.py + homeassistant/components/harmony/util.py homeassistant/components/haveibeenpwned/sensor.py homeassistant/components/hdmi_cec/* homeassistant/components/heatmiser/climate.py @@ -578,13 +590,7 @@ omit = homeassistant/components/neato/vacuum.py homeassistant/components/nederlandse_spoorwegen/sensor.py homeassistant/components/nello/lock.py - homeassistant/components/nest/__init__.py - homeassistant/components/nest/api.py - homeassistant/components/nest/binary_sensor.py - homeassistant/components/nest/camera.py - homeassistant/components/nest/climate.py homeassistant/components/nest/legacy/* - homeassistant/components/nest/sensor.py homeassistant/components/netatmo/__init__.py homeassistant/components/netatmo/api.py homeassistant/components/netatmo/camera.py @@ -629,6 +635,11 @@ omit = homeassistant/components/omnilogic/__init__.py homeassistant/components/omnilogic/common.py homeassistant/components/omnilogic/sensor.py + homeassistant/components/ondilo_ico/__init__.py + homeassistant/components/ondilo_ico/api.py + homeassistant/components/ondilo_ico/const.py + homeassistant/components/ondilo_ico/oauth_impl.py + homeassistant/components/ondilo_ico/sensor.py homeassistant/components/onkyo/media_player.py homeassistant/components/onvif/__init__.py homeassistant/components/onvif/base.py @@ -690,8 +701,6 @@ omit = homeassistant/components/pjlink/media_player.py homeassistant/components/plaato/* homeassistant/components/plex/media_player.py - homeassistant/components/plex/models.py - homeassistant/components/plex/sensor.py homeassistant/components/plum_lightpad/light.py homeassistant/components/pocketcasts/sensor.py homeassistant/components/point/* @@ -706,7 +715,6 @@ omit = homeassistant/components/prowl/notify.py homeassistant/components/proxmoxve/* homeassistant/components/proxy/camera.py - homeassistant/components/ptvsd/* homeassistant/components/pulseaudio_loopback/switch.py homeassistant/components/pushbullet/notify.py homeassistant/components/pushbullet/sensor.py @@ -790,11 +798,9 @@ omit = homeassistant/components/shodan/sensor.py homeassistant/components/shelly/__init__.py homeassistant/components/shelly/binary_sensor.py - homeassistant/components/shelly/cover.py homeassistant/components/shelly/entity.py homeassistant/components/shelly/light.py homeassistant/components/shelly/sensor.py - homeassistant/components/shelly/switch.py homeassistant/components/shelly/utils.py homeassistant/components/sht31/sensor.py homeassistant/components/sigfox/sensor.py @@ -837,7 +843,8 @@ omit = homeassistant/components/soma/cover.py homeassistant/components/soma/sensor.py homeassistant/components/somfy/* - homeassistant/components/somfy_mylink/* + homeassistant/components/somfy_mylink/__init__.py + homeassistant/components/somfy_mylink/cover.py homeassistant/components/sonos/* homeassistant/components/sony_projector/switch.py homeassistant/components/spc/* @@ -1090,6 +1097,9 @@ omit = homeassistant/components/zoneminder/* homeassistant/components/supla/* homeassistant/components/zwave/util.py + homeassistant/components/zwave_js/discovery.py + homeassistant/components/zwave_js/light.py + homeassistant/components/zwave_js/sensor.py [report] # Regexes for lines to exclude from consideration diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a33cd59f227..6356803cbef 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,7 +11,7 @@ on: env: CACHE_VERSION: 1 - DEFAULT_PYTHON: 3.7 + DEFAULT_PYTHON: 3.8 PRE_COMMIT_HOME: ~/.cache/pre-commit jobs: @@ -521,13 +521,12 @@ jobs: needs: prepare-tests strategy: matrix: - python-version: [3.7] + python-version: [3.8] container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub uses: actions/checkout@v2 - - name: - Restore full Python ${{ matrix.python-version }} virtual environment + - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2 with: @@ -585,13 +584,12 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.7, 3.8] + python-version: [3.8, 3.9] container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub uses: actions/checkout@v2 - - name: - Restore full Python ${{ matrix.python-version }} virtual environment + - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2 with: @@ -605,10 +603,13 @@ jobs: ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ matrix.python-version }}-${{ hashFiles('requirements_test.txt') }}-${{ hashFiles('requirements_all.txt') }} ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ matrix.python-version }}-${{ hashFiles('requirements_test.txt') }} ${{ env.CACHE_VERSION}}-${{ runner.os }}-venv-${{ matrix.python-version }}- - - name: - Create full Python ${{ matrix.python-version }} virtual environment + - name: Create full Python ${{ matrix.python-version }} virtual environment if: steps.cache-venv.outputs.cache-hit != 'true' run: | + # Temporary addition of cmake, needed to build some Python 3.9 packages + apt-get update + apt-get -y install cmake + python -m venv venv . venv/bin/activate pip install -U "pip<20.3" setuptools wheel @@ -622,13 +623,12 @@ jobs: needs: prepare-tests strategy: matrix: - python-version: [3.7] + python-version: [3.8] container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub uses: actions/checkout@v2 - - name: - Restore full Python ${{ matrix.python-version }} virtual environment + - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2 with: @@ -657,13 +657,12 @@ jobs: needs: prepare-tests strategy: matrix: - python-version: [3.7] + python-version: [3.8] container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub uses: actions/checkout@v2 - - name: - Restore full Python ${{ matrix.python-version }} virtual environment + - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2 with: @@ -692,15 +691,14 @@ jobs: strategy: matrix: group: [1, 2, 3, 4] - python-version: [3.7, 3.8] + python-version: [3.8, 3.9] name: >- Run tests Python ${{ matrix.python-version }} (group ${{ matrix.group }}) container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub uses: actions/checkout@v2 - - name: - Restore full Python ${{ matrix.python-version }} virtual environment + - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2 with: @@ -741,7 +739,7 @@ jobs: -p no:sugar \ tests - name: Upload coverage artifact - uses: actions/upload-artifact@v2.2.1 + uses: actions/upload-artifact@v2.2.2 with: name: coverage-${{ matrix.python-version }}-group${{ matrix.group }} path: .coverage @@ -755,13 +753,12 @@ jobs: needs: pytest strategy: matrix: - python-version: [3.7] + python-version: [3.8] container: homeassistant/ci-azure:${{ matrix.python-version }} steps: - name: Check out code from GitHub uses: actions/checkout@v2 - - name: - Restore full Python ${{ matrix.python-version }} virtual environment + - name: Restore full Python ${{ matrix.python-version }} virtual environment id: cache-venv uses: actions/cache@v2 with: @@ -785,4 +782,4 @@ jobs: coverage report --fail-under=94 coverage xml - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1.1.1 + uses: codecov/codecov-action@v1.2.1 diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml index 4f7a0efb2d7..3059dc5e2ef 100644 --- a/.github/workflows/lock.yml +++ b/.github/workflows/lock.yml @@ -9,7 +9,7 @@ jobs: lock: runs-on: ubuntu-latest steps: - - uses: dessant/lock-threads@v2.0.1 + - uses: dessant/lock-threads@v2.0.3 with: github-token: ${{ github.token }} issue-lock-inactive-days: "30" diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 519353c81a9..6daeccc4aca 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -4,23 +4,27 @@ name: Stale on: schedule: - cron: "0 * * * *" + workflow_dispatch: jobs: stale: runs-on: ubuntu-latest steps: # The 90 day stale policy - # Used for: Everything (unless 30 day policy below beats it) - - name: 90 days stale policy - uses: actions/stale@v3.0.14 + # Used for: + # - Issues & PRs + # - No PRs marked as no-stale + # - No issues marked as no-stale or help-wanted + - name: 90 days stale issues & PRs policy + uses: actions/stale@v3.0.15 with: repo-token: ${{ secrets.GITHUB_TOKEN }} days-before-stale: 90 days-before-close: 7 - operations-per-run: 25 + operations-per-run: 150 remove-stale-when-updated: true stale-issue-label: "stale" - exempt-issue-labels: "no-stale,Help%20wanted,help-wanted" + exempt-issue-labels: "no-stale,help-wanted" stale-issue-message: > There hasn't been any activity on this issue recently. Due to the high number of incoming GitHub notifications, we have to clean some @@ -43,22 +47,48 @@ jobs: Thank you for your contributions. - # The 30 day stale policy + # The 30 day stale policy for PRS # Used for: - # - Issues that are pending more information (incomplete issues) - # - PRs that are not marked as new-integration - - name: 30 days stale policy - uses: actions/stale@v3.0.14 + # - PRs + # - No PRs marked as no-stale or new-integrations + # - No issues (-1) + - name: 30 days stale PRs policy + uses: actions/stale@v3.0.15 with: repo-token: ${{ secrets.GITHUB_TOKEN }} - # PRs have a CLA signed label, we can misuse it to apply this policy - only-labels: "cla-signed,needs-more-information" days-before-stale: 30 days-before-close: 7 - operations-per-run: 5 + days-before-issue-close: -1 + operations-per-run: 50 + remove-stale-when-updated: true + stale-pr-label: "stale" + # Exempt new integrations, these often take more time. + # They will automatically be handled by the 90 day version above. + exempt-pr-labels: "no-stale,new-integration" + stale-pr-message: > + There hasn't been any activity on this pull request recently. This + pull request has been automatically marked as stale because of that + and will be closed if no further activity occurs within 7 days. + + Thank you for your contributions. + + # The 30 day stale policy for issues + # Used for: + # - Issues that are pending more information (incomplete issues) + # - No Issues marked as no-stale or help-wanted + # - No PRs (-1) + - name: Needs more information stale issues policy + uses: actions/stale@v3.0.15 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + only-labels: "needs-more-information" + days-before-stale: 14 + days-before-close: 7 + days-before-pr-close: -1 + operations-per-run: 50 remove-stale-when-updated: true stale-issue-label: "stale" - exempt-issue-labels: "no-stale,Help%20wanted,help-wanted" + exempt-issue-labels: "no-stale,help-wanted" stale-issue-message: > There hasn't been any activity on this issue recently. Due to the high number of incoming GitHub notifications, we have to clean some @@ -71,14 +101,3 @@ jobs: This issue has now been marked as stale and will be closed if no further activity occurs. Thank you for your contributions. - - stale-pr-label: "stale" - # Exempt new integrations, these often take more time. - # They will automatically be handled by the 90 day version above. - exempt-pr-labels: "no-stale,new-integration" - stale-pr-message: > - There hasn't been any activity on this pull request recently. This - pull request has been automatically marked as stale because of that - and will be closed if no further activity occurs within 7 days. - - Thank you for your contributions. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c96a990433a..8944a69d9ed 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: rev: v2.7.2 hooks: - id: pyupgrade - args: [--py37-plus] + args: [--py38-plus] - repo: https://github.com/psf/black rev: 20.8b1 hooks: @@ -13,14 +13,15 @@ repos: - --quiet files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$ - repo: https://github.com/codespell-project/codespell - rev: v1.17.1 + rev: v2.0.0 hooks: - id: codespell args: - - --ignore-words-list=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,iam,incomfort + - --ignore-words-list=hass,alot,datas,dof,dur,ether,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing,iam,incomfort - --skip="./.*,*.csv,*.json" - --quiet-level=2 exclude_types: [csv, json] + exclude: ^tests/fixtures/ - repo: https://gitlab.com/pycqa/flake8 rev: 3.8.4 hooks: diff --git a/.readthedocs.yml b/.readthedocs.yml index 0303f84d51c..e8344e0a655 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -4,7 +4,7 @@ build: image: latest python: - version: 3.7 + version: 3.8 setup_py_install: true requirements_file: requirements_docs.txt diff --git a/.vscode/settings.default.json b/.vscode/settings.default.json index 85cf4e8b83a..3765d1251b8 100644 --- a/.vscode/settings.default.json +++ b/.vscode/settings.default.json @@ -5,5 +5,5 @@ // https://github.com/microsoft/vscode-python/issues/14067 "python.testing.pytestArgs": ["--no-cov"], // https://code.visualstudio.com/docs/python/testing#_pytest-configuration-settings - "python.testing.pytestEnabled": true + "python.testing.pytestEnabled": false } diff --git a/CODEOWNERS b/CODEOWNERS index a660d930128..b8175614fb5 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -26,6 +26,7 @@ homeassistant/components/adguard/* @frenck homeassistant/components/advantage_air/* @Bre77 homeassistant/components/agent_dvr/* @ispysoftware homeassistant/components/airly/* @bieniu +homeassistant/components/airnow/* @asymworks homeassistant/components/airvisual/* @bachya homeassistant/components/alarmdecoder/* @ajschmidt8 homeassistant/components/alexa/* @home-assistant/cloud @ochlocracy @@ -55,7 +56,6 @@ homeassistant/components/auth/* @home-assistant/core homeassistant/components/automation/* @home-assistant/core homeassistant/components/avea/* @pattyland homeassistant/components/awair/* @ahayworth @danielsjf -homeassistant/components/aws/* @awarecan homeassistant/components/axis/* @Kane610 homeassistant/components/azure_devops/* @timmo001 homeassistant/components/azure_event_hub/* @eavanvalkenburg @@ -107,6 +107,7 @@ homeassistant/components/derivative/* @afaucogney homeassistant/components/device_automation/* @home-assistant/core homeassistant/components/devolo_home_control/* @2Fake @Shutgun homeassistant/components/dexcom/* @gagebenne +homeassistant/components/dhcp/* @bdraco homeassistant/components/digital_ocean/* @fabaff homeassistant/components/directv/* @ctalkington homeassistant/components/discogs/* @thibmaek @@ -119,6 +120,7 @@ homeassistant/components/dweet/* @fabaff homeassistant/components/dynalite/* @ziv1234 homeassistant/components/eafm/* @Jc2k homeassistant/components/ecobee/* @marthoc +homeassistant/components/econet/* @vangorra @w1ll1am23 homeassistant/components/ecovacs/* @OverloadUT homeassistant/components/edl21/* @mtdcr homeassistant/components/egardia/* @jeroenterheerdt @@ -179,7 +181,7 @@ homeassistant/components/griddy/* @bdraco homeassistant/components/group/* @home-assistant/core homeassistant/components/growatt_server/* @indykoning homeassistant/components/guardian/* @bachya -homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco +homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco @mkeesey homeassistant/components/hassio/* @home-assistant/supervisor homeassistant/components/hdmi_cec/* @newAM homeassistant/components/heatmiser/* @andylockran @@ -201,6 +203,7 @@ homeassistant/components/http/* @home-assistant/core homeassistant/components/huawei_lte/* @scop @fphammerle homeassistant/components/huawei_router/* @abmantis homeassistant/components/hue/* @balloob @frenck +homeassistant/components/huisbaasje/* @denniss17 homeassistant/components/humidifier/* @home-assistant/core @Shulyaka homeassistant/components/hunterdouglas_powerview/* @bdraco homeassistant/components/hvv_departures/* @vigonotion @@ -256,7 +259,7 @@ homeassistant/components/luci/* @mzdrale homeassistant/components/luftdaten/* @fabaff homeassistant/components/lupusec/* @majuss homeassistant/components/lutron/* @JonGilmore -homeassistant/components/lutron_caseta/* @swails +homeassistant/components/lutron_caseta/* @swails @bdraco homeassistant/components/mastodon/* @fabaff homeassistant/components/matrix/* @tinloaf homeassistant/components/mcp23017/* @jardiamj @@ -289,10 +292,10 @@ homeassistant/components/neato/* @dshokouhi @Santobert homeassistant/components/nederlandse_spoorwegen/* @YarmoM homeassistant/components/nello/* @pschmitt homeassistant/components/ness_alarm/* @nickw444 -homeassistant/components/nest/* @awarecan @allenporter +homeassistant/components/nest/* @allenporter homeassistant/components/netatmo/* @cgtobi homeassistant/components/netdata/* @fabaff -homeassistant/components/nexia/* @ryannazaretian @bdraco +homeassistant/components/nexia/* @bdraco homeassistant/components/nextbus/* @vividboarder homeassistant/components/nextcloud/* @meichthys homeassistant/components/nightscout/* @marciogranzotto @@ -318,6 +321,7 @@ homeassistant/components/ohmconnect/* @robbiet480 homeassistant/components/ombi/* @larssont homeassistant/components/omnilogic/* @oliver84 @djtimca @gentoosu homeassistant/components/onboarding/* @home-assistant/core +homeassistant/components/ondilo_ico/* @JeromeHXP homeassistant/components/onewire/* @garbled1 @epenet homeassistant/components/onvif/* @hunterjm homeassistant/components/openerz/* @misialq @@ -351,7 +355,6 @@ homeassistant/components/progettihwsw/* @ardaseremet homeassistant/components/prometheus/* @knyar homeassistant/components/proxmoxve/* @k4ds3 @jhollowe homeassistant/components/ps4/* @ktnrg45 -homeassistant/components/ptvsd/* @swamp-ig homeassistant/components/push/* @dgomes homeassistant/components/pvoutput/* @fabaff homeassistant/components/pvpc_hourly_pricing/* @azogue @@ -362,6 +365,7 @@ homeassistant/components/quantum_gateway/* @cisasteelersfan homeassistant/components/qvr_pro/* @oblogic7 homeassistant/components/qwikswitch/* @kellerza homeassistant/components/rachio/* @bdraco +homeassistant/components/radiotherm/* @vinnyfuria homeassistant/components/rainbird/* @konikvranik homeassistant/components/raincloud/* @vanstinator homeassistant/components/rainforest_eagle/* @gtdiehl @jcalbert @@ -396,7 +400,7 @@ homeassistant/components/seven_segments/* @fabaff homeassistant/components/seventeentrack/* @bachya homeassistant/components/sharkiq/* @ajmarks homeassistant/components/shell_command/* @home-assistant/core -homeassistant/components/shelly/* @balloob @bieniu @thecode +homeassistant/components/shelly/* @balloob @bieniu @thecode @chemelli74 homeassistant/components/shiftr/* @fabaff homeassistant/components/shodan/* @fabaff homeassistant/components/sighthound/* @robmarkcole @@ -420,6 +424,7 @@ homeassistant/components/solarlog/* @Ernst79 homeassistant/components/solax/* @squishykid homeassistant/components/soma/* @ratsept homeassistant/components/somfy/* @tetienne +homeassistant/components/somfy_mylink/* @bdraco homeassistant/components/sonarr/* @ctalkington homeassistant/components/songpal/* @rytilahti @shenxn homeassistant/components/sonos/* @cgtobi @@ -535,6 +540,7 @@ homeassistant/components/zodiac/* @JulienTant homeassistant/components/zone/* @home-assistant/core homeassistant/components/zoneminder/* @rohankapoorcom homeassistant/components/zwave/* @home-assistant/z-wave +homeassistant/components/zwave_js/* @home-assistant/z-wave # Individual files homeassistant/components/demo/weather @fabaff diff --git a/Dockerfile b/Dockerfile index cbcc948f5dc..6bcb080a06e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,9 @@ ARG BUILD_FROM FROM ${BUILD_FROM} +# Synchronize with homeassistant/core.py:async_stop ENV \ - S6_SERVICES_GRACETIME=60000 + S6_SERVICES_GRACETIME=220000 WORKDIR /usr/src diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 613e386b249..cda5943ecd0 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -14,8 +14,6 @@ pr: resources: containers: - - container: 37 - image: homeassistant/ci-azure:3.7 - container: 38 image: homeassistant/ci-azure:3.8 repositories: @@ -25,7 +23,7 @@ resources: endpoint: "home-assistant" variables: - name: PythonMain - value: "37" + value: "38" - name: versionHadolint value: "v1.17.6" @@ -150,8 +148,6 @@ stages: strategy: maxParallel: 3 matrix: - Python37: - python.container: "37" Python38: python.container: "38" container: $[ variables['python.container'] ] diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 6da0b128e47..418fdf5b26c 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -60,9 +60,9 @@ stages: vmImage: 'ubuntu-latest' steps: - task: UsePythonVersion@0 - displayName: 'Use Python 3.7' + displayName: 'Use Python 3.8' inputs: - versionSpec: '3.7' + versionSpec: '3.8' - script: pip install twine wheel displayName: 'Install tools' - script: python setup.py sdist bdist_wheel diff --git a/azure-pipelines-translation.yml b/azure-pipelines-translation.yml index 9f4db8d2005..481b98bc484 100644 --- a/azure-pipelines-translation.yml +++ b/azure-pipelines-translation.yml @@ -30,9 +30,9 @@ jobs: vmImage: 'ubuntu-latest' steps: - task: UsePythonVersion@0 - displayName: 'Use Python 3.7' + displayName: 'Use Python 3.8' inputs: - versionSpec: '3.7' + versionSpec: '3.8' - script: | export LOKALISE_TOKEN="$(lokaliseToken)" export AZURE_BRANCH="$(Build.SourceBranchName)" diff --git a/build.json b/build.json index a7ce097ae84..0183b61c67c 100644 --- a/build.json +++ b/build.json @@ -1,11 +1,11 @@ { "image": "homeassistant/{arch}-homeassistant", "build_from": { - "aarch64": "homeassistant/aarch64-homeassistant-base:2021.01.0", - "armhf": "homeassistant/armhf-homeassistant-base:2021.01.0", - "armv7": "homeassistant/armv7-homeassistant-base:2021.01.0", - "amd64": "homeassistant/amd64-homeassistant-base:2021.01.0", - "i386": "homeassistant/i386-homeassistant-base:2021.01.0" + "aarch64": "homeassistant/aarch64-homeassistant-base:2021.02.0", + "armhf": "homeassistant/armhf-homeassistant-base:2021.02.0", + "armv7": "homeassistant/armv7-homeassistant-base:2021.02.0", + "amd64": "homeassistant/amd64-homeassistant-base:2021.02.0", + "i386": "homeassistant/i386-homeassistant-base:2021.02.0" }, "labels": { "io.hass.type": "core" diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index b68fc9d17ac..0f5bda7fbf2 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -48,7 +48,7 @@ COOLDOWN_TIME = 60 MAX_LOAD_CONCURRENTLY = 6 -DEBUGGER_INTEGRATIONS = {"debugpy", "ptvsd"} +DEBUGGER_INTEGRATIONS = {"debugpy"} CORE_INTEGRATIONS = ("homeassistant", "persistent_notification") LOGGING_INTEGRATIONS = { # Set log levels @@ -307,12 +307,10 @@ def async_enable_logging( sys.excepthook = lambda *args: logging.getLogger(None).exception( "Uncaught exception", exc_info=args # type: ignore ) - - if sys.version_info[:2] >= (3, 8): - threading.excepthook = lambda args: logging.getLogger(None).exception( - "Uncaught thread exception", - exc_info=(args.exc_type, args.exc_value, args.exc_traceback), - ) + threading.excepthook = lambda args: logging.getLogger(None).exception( + "Uncaught thread exception", + exc_info=(args.exc_type, args.exc_value, args.exc_traceback), # type: ignore[arg-type] + ) # Log errors to a file if we have write access to file or config dir if log_file is None: @@ -383,7 +381,7 @@ def _get_domains(hass: core.HomeAssistant, config: Dict[str, Any]) -> Set[str]: async def _async_log_pending_setups( - domains: Set[str], setup_started: Dict[str, datetime] + hass: core.HomeAssistant, domains: Set[str], setup_started: Dict[str, datetime] ) -> None: """Periodic log of setups that are pending for longer than LOG_SLOW_STARTUP_INTERVAL.""" while True: @@ -395,6 +393,7 @@ async def _async_log_pending_setups( "Waiting on integrations to complete setup: %s", ", ".join(remaining), ) + _LOGGER.debug("Running timeout Zones: %s", hass.timeout.zones) async def async_setup_multi_components( @@ -408,7 +407,9 @@ async def async_setup_multi_components( domain: hass.async_create_task(async_setup_component(hass, domain, config)) for domain in domains } - log_task = asyncio.create_task(_async_log_pending_setups(domains, setup_started)) + log_task = asyncio.create_task( + _async_log_pending_setups(hass, domains, setup_started) + ) await asyncio.wait(futures.values()) log_task.cancel() errors = [domain for domain in domains if futures[domain].exception()] diff --git a/homeassistant/components/abode/translations/de.json b/homeassistant/components/abode/translations/de.json index 43d6ba21ca5..307f5f45065 100644 --- a/homeassistant/components/abode/translations/de.json +++ b/homeassistant/components/abode/translations/de.json @@ -2,7 +2,7 @@ "config": { "abort": { "reauth_successful": "Die erneute Authentifizierung war erfolgreich", - "single_instance_allowed": "Es ist nur eine einzige Konfiguration von Abode erlaubt." + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", diff --git a/homeassistant/components/abode/translations/es.json b/homeassistant/components/abode/translations/es.json index 9fa8cd8b06b..66cb5d13f22 100644 --- a/homeassistant/components/abode/translations/es.json +++ b/homeassistant/components/abode/translations/es.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "reauth_successful": "La reautenticaci\u00f3n fue exitosa", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente", "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { @@ -19,7 +19,7 @@ "reauth_confirm": { "data": { "password": "Contrase\u00f1a", - "username": "Correo electronico" + "username": "Correo electr\u00f3nico" }, "title": "Rellene su informaci\u00f3n de inicio de sesi\u00f3n de Abode" }, diff --git a/homeassistant/components/abode/translations/fr.json b/homeassistant/components/abode/translations/fr.json index 87be79571a4..2ab158cca57 100644 --- a/homeassistant/components/abode/translations/fr.json +++ b/homeassistant/components/abode/translations/fr.json @@ -1,17 +1,32 @@ { "config": { "abort": { - "single_instance_allowed": "Une seule configuration d'Abode est autoris\u00e9e." + "reauth_successful": "La r\u00e9-authentification a r\u00e9ussi", + "single_instance_allowed": "D\u00e9ja configur\u00e9. Une seule configuration possible." }, "error": { "cannot_connect": "\u00c9chec de connexion", - "invalid_auth": "Authentification invalide" + "invalid_auth": "Authentification invalide", + "invalid_mfa_code": "Code MFA non valide" }, "step": { + "mfa": { + "data": { + "mfa_code": "Code MFA (6 chiffres)" + }, + "title": "Entrez votre code MFA pour Abode" + }, + "reauth_confirm": { + "data": { + "password": "Mot de passe", + "username": "Email" + }, + "title": "Remplissez vos informations de connexion Abode" + }, "user": { "data": { "password": "Mot de passe", - "username": "Adresse e-mail" + "username": "Email" }, "title": "Remplissez vos informations de connexion Abode" } diff --git a/homeassistant/components/abode/translations/tr.json b/homeassistant/components/abode/translations/tr.json new file mode 100644 index 00000000000..d469e43f1f4 --- /dev/null +++ b/homeassistant/components/abode/translations/tr.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_mfa_code": "Ge\u00e7ersiz MFA kodu" + }, + "step": { + "mfa": { + "data": { + "mfa_code": "MFA kodu (6 basamakl\u0131)" + }, + "title": "Abode i\u00e7in MFA kodunuzu girin" + }, + "reauth_confirm": { + "data": { + "password": "Parola", + "username": "E-posta" + }, + "title": "Abode giri\u015f bilgilerinizi doldurun" + }, + "user": { + "data": { + "password": "Parola", + "username": "E-posta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/abode/translations/uk.json b/homeassistant/components/abode/translations/uk.json new file mode 100644 index 00000000000..7ad57a0ec68 --- /dev/null +++ b/homeassistant/components/abode/translations/uk.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e", + "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." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "invalid_mfa_code": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439 \u043a\u043e\u0434 MFA." + }, + "step": { + "mfa": { + "data": { + "mfa_code": "\u041a\u043e\u0434 MFA (6 \u0446\u0438\u0444\u0440)" + }, + "title": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043a\u043e\u0434 MFA \u0434\u043b\u044f Abode" + }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438" + }, + "title": "Abode" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438" + }, + "title": "Abode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/accuweather/__init__.py b/homeassistant/components/accuweather/__init__.py index c8ae14678d5..27dbae7a41f 100644 --- a/homeassistant/components/accuweather/__init__.py +++ b/homeassistant/components/accuweather/__init__.py @@ -100,13 +100,13 @@ class AccuWeatherDataUpdateCoordinator(DataUpdateCoordinator): self.accuweather = AccuWeather(api_key, session, location_key=self.location_key) # Enabling the forecast download increases the number of requests per data - # update, we use 32 minutes for current condition only and 64 minutes for + # update, we use 40 minutes for current condition only and 80 minutes for # current condition and forecast as update interval to not exceed allowed number - # of requests. We have 50 requests allowed per day, so we use 45 and leave 5 as + # of requests. We have 50 requests allowed per day, so we use 36 and leave 14 as # a reserve for restarting HA. - update_interval = ( - timedelta(minutes=64) if self.forecast else timedelta(minutes=32) - ) + update_interval = timedelta(minutes=40) + if self.forecast: + update_interval *= 2 _LOGGER.debug("Data will be update every %s", update_interval) super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) diff --git a/homeassistant/components/accuweather/const.py b/homeassistant/components/accuweather/const.py index cbccc3a462d..e8dbe921d77 100644 --- a/homeassistant/components/accuweather/const.py +++ b/homeassistant/components/accuweather/const.py @@ -52,12 +52,12 @@ CONDITION_CLASSES = { ATTR_CONDITION_HAIL: [25], ATTR_CONDITION_LIGHTNING: [15], ATTR_CONDITION_LIGHTNING_RAINY: [16, 17, 41, 42], - ATTR_CONDITION_PARTLYCLOUDY: [4, 6, 35, 36], + ATTR_CONDITION_PARTLYCLOUDY: [3, 4, 6, 35, 36], ATTR_CONDITION_POURING: [18], ATTR_CONDITION_RAINY: [12, 13, 14, 26, 39, 40], ATTR_CONDITION_SNOWY: [19, 20, 21, 22, 23, 43, 44], ATTR_CONDITION_SNOWY_RAINY: [29], - ATTR_CONDITION_SUNNY: [1, 2, 3, 5], + ATTR_CONDITION_SUNNY: [1, 2, 5], ATTR_CONDITION_WINDY: [32], } diff --git a/homeassistant/components/accuweather/strings.json b/homeassistant/components/accuweather/strings.json index 65aa2a9ed91..c4305a0a7a5 100644 --- a/homeassistant/components/accuweather/strings.json +++ b/homeassistant/components/accuweather/strings.json @@ -25,7 +25,7 @@ "step": { "user": { "title": "AccuWeather Options", - "description": "Due to the limitations of the free version of the AccuWeather API key, when you enable weather forecast, data updates will be performed every 64 minutes instead of every 32 minutes.", + "description": "Due to the limitations of the free version of the AccuWeather API key, when you enable weather forecast, data updates will be performed every 80 minutes instead of every 40 minutes.", "data": { "forecast": "Weather forecast" } diff --git a/homeassistant/components/accuweather/translations/ca.json b/homeassistant/components/accuweather/translations/ca.json index 9c33637baa8..8178a5caef0 100644 --- a/homeassistant/components/accuweather/translations/ca.json +++ b/homeassistant/components/accuweather/translations/ca.json @@ -27,7 +27,7 @@ "data": { "forecast": "Previsi\u00f3 meteorol\u00f2gica" }, - "description": "Per culpa de les limitacions de la versi\u00f3 gratu\u00efta l'API d'AccuWeather, quan habilitis la previsi\u00f3 meteorol\u00f2gica, les actualitzacions es realitzaran cada 64 minuts en comptes de 32.", + "description": "Per culpa de les limitacions de la versi\u00f3 gratu\u00efta l'API d'AccuWeather, quan habilitis la previsi\u00f3 meteorol\u00f2gica, les actualitzacions de dades es faran cada 80 minuts en comptes de cada 40.", "title": "Opcions d'AccuWeather" } } diff --git a/homeassistant/components/accuweather/translations/cs.json b/homeassistant/components/accuweather/translations/cs.json index ea954b9f0db..1cf34a42695 100644 --- a/homeassistant/components/accuweather/translations/cs.json +++ b/homeassistant/components/accuweather/translations/cs.json @@ -27,7 +27,7 @@ "data": { "forecast": "P\u0159edpov\u011b\u010f po\u010das\u00ed" }, - "description": "Kdy\u017e povol\u00edte p\u0159edpov\u011b\u010f po\u010das\u00ed, budou aktualizace dat prov\u00e1d\u011bny ka\u017ed\u00fdch 64 minut nam\u00edsto 32 minut z d\u016fvodu omezen\u00ed bezplatn\u00e9 verze AccuWeather.", + "description": "Kdy\u017e povol\u00edte p\u0159edpov\u011b\u010f po\u010das\u00ed, budou aktualizace dat prov\u00e1d\u011bny ka\u017ed\u00fdch 80 minut nam\u00edsto 40 minut z d\u016fvodu omezen\u00ed bezplatn\u00e9 verze AccuWeather.", "title": "Mo\u017enosti AccuWeather" } } diff --git a/homeassistant/components/accuweather/translations/de.json b/homeassistant/components/accuweather/translations/de.json index fe0319764a7..814e57d1d6c 100644 --- a/homeassistant/components/accuweather/translations/de.json +++ b/homeassistant/components/accuweather/translations/de.json @@ -1,11 +1,16 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel" }, "step": { "user": { "data": { + "api_key": "API-Schl\u00fcssel", "latitude": "Breitengrad", "longitude": "L\u00e4ngengrad", "name": "Name" @@ -25,7 +30,7 @@ }, "system_health": { "info": { - "can_reach_server": "AccuWeather Server erreichen", + "can_reach_server": "AccuWeather-Server erreichen", "remaining_requests": "Verbleibende erlaubte Anfragen" } } diff --git a/homeassistant/components/accuweather/translations/en.json b/homeassistant/components/accuweather/translations/en.json index b737c420a2d..8f2261b93c7 100644 --- a/homeassistant/components/accuweather/translations/en.json +++ b/homeassistant/components/accuweather/translations/en.json @@ -27,7 +27,7 @@ "data": { "forecast": "Weather forecast" }, - "description": "Due to the limitations of the free version of the AccuWeather API key, when you enable weather forecast, data updates will be performed every 64 minutes instead of every 32 minutes.", + "description": "Due to the limitations of the free version of the AccuWeather API key, when you enable weather forecast, data updates will be performed every 80 minutes instead of every 40 minutes.", "title": "AccuWeather Options" } } diff --git a/homeassistant/components/accuweather/translations/et.json b/homeassistant/components/accuweather/translations/et.json index bed28b62975..6e2dc1ffd96 100644 --- a/homeassistant/components/accuweather/translations/et.json +++ b/homeassistant/components/accuweather/translations/et.json @@ -27,7 +27,7 @@ "data": { "forecast": "Ilmateade" }, - "description": "AccuWeather API tasuta versioonis toimub ilmaennustuse lubamisel andmete v\u00e4rskendamine iga 32 minuti asemel iga 64 minuti j\u00e4rel.", + "description": "AccuWeather API tasuta versioonis toimub ilmaennustuse lubamisel andmete v\u00e4rskendamine iga 80 minuti j\u00e4rel (muidu 40 minutit).", "title": "AccuWeatheri valikud" } } diff --git a/homeassistant/components/accuweather/translations/fr.json b/homeassistant/components/accuweather/translations/fr.json index 8e638205417..a083ed09bdf 100644 --- a/homeassistant/components/accuweather/translations/fr.json +++ b/homeassistant/components/accuweather/translations/fr.json @@ -34,7 +34,8 @@ }, "system_health": { "info": { - "can_reach_server": "Acc\u00e8s au serveur AccuWeather" + "can_reach_server": "Acc\u00e8s au serveur AccuWeather", + "remaining_requests": "Demandes restantes autoris\u00e9es" } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/it.json b/homeassistant/components/accuweather/translations/it.json index 86aaa213a15..8a1f9b96463 100644 --- a/homeassistant/components/accuweather/translations/it.json +++ b/homeassistant/components/accuweather/translations/it.json @@ -27,7 +27,7 @@ "data": { "forecast": "Previsioni meteo" }, - "description": "A causa delle limitazioni della versione gratuita della chiave API AccuWeather, quando si abilitano le previsioni del tempo, gli aggiornamenti dei dati verranno eseguiti ogni 64 minuti invece che ogni 32 minuti.", + "description": "A causa delle limitazioni della versione gratuita della chiave API AccuWeather, quando si abilitano le previsioni del tempo, gli aggiornamenti dei dati verranno eseguiti ogni 80 minuti invece che ogni 40.", "title": "Opzioni AccuWeather" } } diff --git a/homeassistant/components/accuweather/translations/no.json b/homeassistant/components/accuweather/translations/no.json index 50482cb3e61..be87b1ab244 100644 --- a/homeassistant/components/accuweather/translations/no.json +++ b/homeassistant/components/accuweather/translations/no.json @@ -27,7 +27,7 @@ "data": { "forecast": "V\u00e6rmelding" }, - "description": "P\u00e5 grunn av begrensningene i gratisversjonen av AccuWeather API-n\u00f8kkelen, n\u00e5r du aktiverer v\u00e6rmelding, vil dataoppdateringer bli utf\u00f8rt hvert 64. minutt i stedet for hvert 32. minutt.", + "description": "P\u00e5 grunn av begrensningene i den gratis versjonen av AccuWeather API-n\u00f8kkelen, vil dataoppdateringer utf\u00f8res hvert 80. minutt i stedet for hvert 40. minutt n\u00e5r du aktiverer v\u00e6rmelding.", "title": "AccuWeather-alternativer" } } diff --git a/homeassistant/components/accuweather/translations/pl.json b/homeassistant/components/accuweather/translations/pl.json index c6e4fb3ba82..2794bc8b7b6 100644 --- a/homeassistant/components/accuweather/translations/pl.json +++ b/homeassistant/components/accuweather/translations/pl.json @@ -27,7 +27,7 @@ "data": { "forecast": "Prognoza pogody" }, - "description": "Ze wzgl\u0119du na ograniczenia darmowej wersji klucza API AccuWeather po w\u0142\u0105czeniu prognozy pogody aktualizacje danych b\u0119d\u0105 wykonywane co 64 minuty zamiast co 32 minuty.", + "description": "Ze wzgl\u0119du na ograniczenia darmowej wersji klucza API AccuWeather po w\u0142\u0105czeniu prognozy pogody aktualizacje danych b\u0119d\u0105 wykonywane co 80 minut zamiast co 40 minut.", "title": "Opcje AccuWeather" } } diff --git a/homeassistant/components/accuweather/translations/ru.json b/homeassistant/components/accuweather/translations/ru.json index 6a675c17248..7bc767b1baf 100644 --- a/homeassistant/components/accuweather/translations/ru.json +++ b/homeassistant/components/accuweather/translations/ru.json @@ -27,7 +27,7 @@ "data": { "forecast": "\u041f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u044b" }, - "description": "\u0412 \u0441\u0432\u044f\u0437\u0438 \u0441 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f\u043c\u0438 \u0431\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 \u043a\u043b\u044e\u0447\u0430 API AccuWeather, \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430 \u043f\u043e\u0433\u043e\u0434\u044b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442\u044c \u043a\u0430\u0436\u0434\u044b\u0435 64 \u043c\u0438\u043d\u0443\u0442\u044b, \u0430 \u043d\u0435 \u043a\u0430\u0436\u0434\u044b\u0435 32 \u043c\u0438\u043d\u0443\u0442\u044b.", + "description": "\u0412 \u0441\u0432\u044f\u0437\u0438 \u0441 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u0438\u044f\u043c\u0438 \u0431\u0435\u0441\u043f\u043b\u0430\u0442\u043d\u043e\u0439 \u0432\u0435\u0440\u0441\u0438\u0438 \u043a\u043b\u044e\u0447\u0430 API AccuWeather, \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0430 \u043f\u043e\u0433\u043e\u0434\u044b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435 \u0434\u0430\u043d\u043d\u044b\u0445 \u0431\u0443\u0434\u0435\u0442 \u043f\u0440\u043e\u0438\u0441\u0445\u043e\u0434\u0438\u0442\u044c \u043a\u0430\u0436\u0434\u044b\u0435 80 \u043c\u0438\u043d\u0443\u0442, \u0430 \u043d\u0435 \u043a\u0430\u0436\u0434\u044b\u0435 40 \u043c\u0438\u043d\u0443\u0442.", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 AccuWeather" } } diff --git a/homeassistant/components/accuweather/translations/sensor.uk.json b/homeassistant/components/accuweather/translations/sensor.uk.json new file mode 100644 index 00000000000..81243e0b05d --- /dev/null +++ b/homeassistant/components/accuweather/translations/sensor.uk.json @@ -0,0 +1,9 @@ +{ + "state": { + "accuweather__pressure_tendency": { + "falling": "\u0417\u043d\u0438\u0436\u0435\u043d\u043d\u044f", + "rising": "\u0417\u0440\u043e\u0441\u0442\u0430\u043d\u043d\u044f", + "steady": "\u0421\u0442\u0430\u0431\u0456\u043b\u044c\u043d\u0438\u0439" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/tr.json b/homeassistant/components/accuweather/translations/tr.json new file mode 100644 index 00000000000..f79f9a0e327 --- /dev/null +++ b/homeassistant/components/accuweather/translations/tr.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "latitude": "Enlem", + "longitude": "Boylam", + "name": "Ad" + }, + "title": "AccuWeather" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "forecast": "Hava Durumu tahmini" + }, + "title": "AccuWeather Se\u00e7enekleri" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "AccuWeather sunucusuna ula\u015f\u0131n", + "remaining_requests": "Kalan izin verilen istekler" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/uk.json b/homeassistant/components/accuweather/translations/uk.json index 8c3f282b350..7432d0df484 100644 --- a/homeassistant/components/accuweather/translations/uk.json +++ b/homeassistant/components/accuweather/translations/uk.json @@ -1,15 +1,22 @@ { "config": { + "abort": { + "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." + }, "error": { - "invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API" + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API", + "requests_exceeded": "\u041f\u0435\u0440\u0435\u0432\u0438\u0449\u0435\u043d\u043e \u0434\u043e\u043f\u0443\u0441\u0442\u0438\u043c\u0443 \u043a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u0437\u0430\u043f\u0438\u0442\u0456\u0432 \u0434\u043e API Accuweather. \u041d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u043f\u043e\u0447\u0435\u043a\u0430\u0442\u0438 \u0430\u0431\u043e \u0437\u043c\u0456\u043d\u0438\u0442\u0438 \u043a\u043b\u044e\u0447 API." }, "step": { "user": { "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", - "name": "\u041d\u0430\u0437\u0432\u0430 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457" + "name": "\u041d\u0430\u0437\u0432\u0430" }, + "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438, \u044f\u043a\u0449\u043e \u0412\u0430\u043c \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u0430 \u0437 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u043c:\n https://www.home-assistant.io/integrations/accuweather/ \n\n\u0417\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0434\u0435\u044f\u043a\u0456 \u0441\u0435\u043d\u0441\u043e\u0440\u0438 \u043f\u0440\u0438\u0445\u043e\u0432\u0430\u043d\u0456 \u0456 \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439 \u043f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u0438. \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0430\u043a\u0442\u0438\u0432\u0443\u0432\u0430\u0442\u0438 \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u043d\u044f \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u0438\u0445 \u0441\u0435\u043d\u0441\u043e\u0440\u0456\u0432 \u0432 \u0440\u0435\u0454\u0441\u0442\u0440\u0456 \u043e\u0431'\u0454\u043a\u0442\u0456\u0432 \u0456 \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u043f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u0438 \u0432 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u0445 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457.", "title": "AccuWeather" } } @@ -19,8 +26,16 @@ "user": { "data": { "forecast": "\u041f\u0440\u043e\u0433\u043d\u043e\u0437 \u043f\u043e\u0433\u043e\u0434\u0438" - } + }, + "description": "\u0423 \u0437\u0432'\u044f\u0437\u043a\u0443 \u0437 \u043e\u0431\u043c\u0435\u0436\u0435\u043d\u043d\u044f\u043c\u0438 \u0431\u0435\u0437\u043a\u043e\u0448\u0442\u043e\u0432\u043d\u043e\u0457 \u0432\u0435\u0440\u0441\u0456\u0457 \u043a\u043b\u044e\u0447\u0430 API AccuWeather, \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u0456 \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0443 \u043f\u043e\u0433\u043e\u0434\u0438 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0434\u0430\u043d\u0438\u0445 \u0431\u0443\u0434\u0435 \u0432\u0456\u0434\u0431\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u043a\u043e\u0436\u043d\u0456 64 \u0445\u0432\u0438\u043b\u0438\u043d\u0438, \u0430 \u043d\u0435 \u043a\u043e\u0436\u043d\u0456 32 \u0445\u0432\u0438\u043b\u0438\u043d\u0438.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AccuWeather" } } + }, + "system_health": { + "info": { + "can_reach_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 AccuWeather", + "remaining_requests": "\u0417\u0430\u043f\u0438\u0442\u0456\u0432 \u0437\u0430\u043b\u0438\u0448\u0438\u043b\u043e\u0441\u044c" + } } } \ No newline at end of file diff --git a/homeassistant/components/accuweather/translations/zh-Hant.json b/homeassistant/components/accuweather/translations/zh-Hant.json index ed5fa26f0c0..eb3729fd2c4 100644 --- a/homeassistant/components/accuweather/translations/zh-Hant.json +++ b/homeassistant/components/accuweather/translations/zh-Hant.json @@ -27,7 +27,7 @@ "data": { "forecast": "\u5929\u6c23\u9810\u5831" }, - "description": "\u7531\u65bc AccuWeather API \u5bc6\u9470\u514d\u8cbb\u7248\u672c\u9650\u5236\uff0c\u7576\u958b\u555f\u5929\u6c23\u9810\u5831\u6642\u3001\u6578\u64da\u6703\u6bcf 64 \u5206\u9418\u66f4\u65b0\u4e00\u6b21\uff0c\u800c\u975e 32 \u5206\u9418\u3002", + "description": "\u7531\u65bc AccuWeather API \u5bc6\u9470\u514d\u8cbb\u7248\u672c\u9650\u5236\uff0c\u7576\u958b\u555f\u5929\u6c23\u9810\u5831\u6642\u3001\u6578\u64da\u6703\u6bcf 80 \u5206\u9418\u66f4\u65b0\u4e00\u6b21\uff0c\u800c\u975e 40 \u5206\u9418\u3002", "title": "AccuWeather \u9078\u9805" } } diff --git a/homeassistant/components/acmeda/base.py b/homeassistant/components/acmeda/base.py index c467fe17ba3..b325e2c944a 100644 --- a/homeassistant/components/acmeda/base.py +++ b/homeassistant/components/acmeda/base.py @@ -26,9 +26,7 @@ class AcmedaBase(entity.Entity): ent_registry.async_remove(self.entity_id) dev_registry = await get_dev_reg(self.hass) - device = dev_registry.async_get_device( - identifiers={(DOMAIN, self.unique_id)}, connections=set() - ) + device = dev_registry.async_get_device(identifiers={(DOMAIN, self.unique_id)}) if device is not None: dev_registry.async_update_device( device.id, remove_config_entry_id=self.registry_entry.config_entry_id diff --git a/homeassistant/components/acmeda/helpers.py b/homeassistant/components/acmeda/helpers.py index cec971e5fdd..1162aba5dc8 100644 --- a/homeassistant/components/acmeda/helpers.py +++ b/homeassistant/components/acmeda/helpers.py @@ -32,9 +32,7 @@ async def update_devices(hass, config_entry, api): for api_item in api.values(): # Update Device name - device = dev_registry.async_get_device( - identifiers={(DOMAIN, api_item.id)}, connections=set() - ) + device = dev_registry.async_get_device(identifiers={(DOMAIN, api_item.id)}) if device is not None: dev_registry.async_update_device( device.id, diff --git a/homeassistant/components/acmeda/translations/de.json b/homeassistant/components/acmeda/translations/de.json index 86b22e47cda..94834cde427 100644 --- a/homeassistant/components/acmeda/translations/de.json +++ b/homeassistant/components/acmeda/translations/de.json @@ -1,11 +1,14 @@ { "config": { + "abort": { + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + }, "step": { "user": { "data": { "id": "Host-ID" }, - "title": "W\u00e4hlen Sie einen Hub zum Hinzuf\u00fcgen aus" + "title": "W\u00e4hle einen Hub zum Hinzuf\u00fcgen aus" } } } diff --git a/homeassistant/components/acmeda/translations/tr.json b/homeassistant/components/acmeda/translations/tr.json new file mode 100644 index 00000000000..aea81abdcba --- /dev/null +++ b/homeassistant/components/acmeda/translations/tr.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "id": "Ana bilgisayar kimli\u011fi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/acmeda/translations/uk.json b/homeassistant/components/acmeda/translations/uk.json new file mode 100644 index 00000000000..245428e9c73 --- /dev/null +++ b/homeassistant/components/acmeda/translations/uk.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456." + }, + "step": { + "user": { + "data": { + "id": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0445\u043e\u0441\u0442\u0430" + }, + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0445\u0430\u0431, \u044f\u043a\u0438\u0439 \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0434\u043e\u0434\u0430\u0442\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/translations/de.json b/homeassistant/components/adguard/translations/de.json index a02601759be..67746b3abcf 100644 --- a/homeassistant/components/adguard/translations/de.json +++ b/homeassistant/components/adguard/translations/de.json @@ -2,10 +2,10 @@ "config": { "abort": { "existing_instance_updated": "Bestehende Konfiguration wurde aktualisiert.", - "single_instance_allowed": "Es ist nur eine einzige Konfiguration von AdGuard Home zul\u00e4ssig." + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "hassio_confirm": { @@ -19,7 +19,7 @@ "port": "Port", "ssl": "AdGuard Home verwendet ein SSL-Zertifikat", "username": "Benutzername", - "verify_ssl": "AdGuard Home verwendet ein richtiges Zertifikat" + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" }, "description": "Richte deine AdGuard Home-Instanz ein um sie zu \u00dcberwachen und zu Steuern." } diff --git a/homeassistant/components/adguard/translations/no.json b/homeassistant/components/adguard/translations/no.json index f5aeea990c3..25046c8d38f 100644 --- a/homeassistant/components/adguard/translations/no.json +++ b/homeassistant/components/adguard/translations/no.json @@ -9,8 +9,8 @@ }, "step": { "hassio_confirm": { - "description": "Vil du konfigurere Home Assistant til \u00e5 koble til AdGuard Hjem gitt av hass.io tillegget {addon}?", - "title": "AdGuard Hjem via Hass.io tillegg" + "description": "Vil du konfigurere Home Assistant til \u00e5 koble til AdGuard Home gitt av Hass.io-tillegg {addon}?", + "title": "AdGuard Home via Hass.io-tillegg" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/ru.json b/homeassistant/components/adguard/translations/ru.json index 34c56342b5b..5e8483047f8 100644 --- a/homeassistant/components/adguard/translations/ru.json +++ b/homeassistant/components/adguard/translations/ru.json @@ -9,8 +9,8 @@ }, "step": { "hassio_confirm": { - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a AdGuard Home (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io \"{addon}\")?", - "title": "AdGuard Home (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io)" + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a AdGuard Home (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant \"{addon}\")?", + "title": "AdGuard Home (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant)" }, "user": { "data": { diff --git a/homeassistant/components/adguard/translations/tr.json b/homeassistant/components/adguard/translations/tr.json new file mode 100644 index 00000000000..26bef46408a --- /dev/null +++ b/homeassistant/components/adguard/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/adguard/translations/uk.json b/homeassistant/components/adguard/translations/uk.json new file mode 100644 index 00000000000..8c24fb0a877 --- /dev/null +++ b/homeassistant/components/adguard/translations/uk.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "existing_instance_updated": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u0430.", + "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." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "hassio_confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e AdGuard Home (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Hass.io \"{addon}\")?", + "title": "AdGuard Home (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Hass.io)" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", + "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433\u0443 \u0456 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044e AdGuard Home." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/advantage_air/translations/de.json b/homeassistant/components/advantage_air/translations/de.json index 0d8a0052406..3b4066996eb 100644 --- a/homeassistant/components/advantage_air/translations/de.json +++ b/homeassistant/components/advantage_air/translations/de.json @@ -1,7 +1,10 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "user": { diff --git a/homeassistant/components/advantage_air/translations/tr.json b/homeassistant/components/advantage_air/translations/tr.json new file mode 100644 index 00000000000..db639c59376 --- /dev/null +++ b/homeassistant/components/advantage_air/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "ip_address": "\u0130p Adresi", + "port": "Port" + }, + "title": "Ba\u011flan" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/advantage_air/translations/uk.json b/homeassistant/components/advantage_air/translations/uk.json new file mode 100644 index 00000000000..14ac18395e2 --- /dev/null +++ b/homeassistant/components/advantage_air/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e API \u0412\u0430\u0448\u043e\u0433\u043e \u043d\u0430\u0441\u0442\u0456\u043d\u043d\u043e\u0433\u043e \u043f\u043b\u0430\u043d\u0448\u0435\u0442\u0430 Advantage Air.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/de.json b/homeassistant/components/agent_dvr/translations/de.json index 6ea40d0fd00..10a8307ada1 100644 --- a/homeassistant/components/agent_dvr/translations/de.json +++ b/homeassistant/components/agent_dvr/translations/de.json @@ -4,8 +4,8 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "already_in_progress": "Der Konfigurationsfluss f\u00fcr das Ger\u00e4t wird bereits ausgef\u00fchrt.", - "cannot_connect": "Verbindungsfehler" + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "user": { diff --git a/homeassistant/components/agent_dvr/translations/tr.json b/homeassistant/components/agent_dvr/translations/tr.json new file mode 100644 index 00000000000..31dddab7795 --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + }, + "title": "Agent DVR'\u0131 kurun" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/agent_dvr/translations/uk.json b/homeassistant/components/agent_dvr/translations/uk.json new file mode 100644 index 00000000000..fef8d45d5a4 --- /dev/null +++ b/homeassistant/components/agent_dvr/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "Agent DVR" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index de09d767b1f..9d6b46f82e5 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -20,6 +20,7 @@ from .const import ( ATTR_API_CAQI, ATTR_API_CAQI_DESCRIPTION, ATTR_API_CAQI_LEVEL, + CONF_USE_NEAREST, DOMAIN, MAX_REQUESTS_PER_DAY, NO_AIRLY_SENSORS, @@ -53,6 +54,7 @@ async def async_setup_entry(hass, config_entry): api_key = config_entry.data[CONF_API_KEY] latitude = config_entry.data[CONF_LATITUDE] longitude = config_entry.data[CONF_LONGITUDE] + use_nearest = config_entry.data.get(CONF_USE_NEAREST, False) # For backwards compat, set unique ID if config_entry.unique_id is None: @@ -67,7 +69,7 @@ async def async_setup_entry(hass, config_entry): ) coordinator = AirlyDataUpdateCoordinator( - hass, websession, api_key, latitude, longitude, update_interval + hass, websession, api_key, latitude, longitude, update_interval, use_nearest ) await coordinator.async_refresh() @@ -107,21 +109,36 @@ async def async_unload_entry(hass, config_entry): class AirlyDataUpdateCoordinator(DataUpdateCoordinator): """Define an object to hold Airly data.""" - def __init__(self, hass, session, api_key, latitude, longitude, update_interval): + def __init__( + self, + hass, + session, + api_key, + latitude, + longitude, + update_interval, + use_nearest, + ): """Initialize.""" self.latitude = latitude self.longitude = longitude self.airly = Airly(api_key, session) + self.use_nearest = use_nearest super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) async def _async_update_data(self): """Update data via library.""" data = {} - with async_timeout.timeout(20): + if self.use_nearest: + measurements = self.airly.create_measurements_session_nearest( + self.latitude, self.longitude, max_distance_km=5 + ) + else: measurements = self.airly.create_measurements_session_point( self.latitude, self.longitude ) + with async_timeout.timeout(20): try: await measurements.update() except (AirlyError, ClientConnectorError) as error: diff --git a/homeassistant/components/airly/air_quality.py b/homeassistant/components/airly/air_quality.py index 4a3de1e6543..e43a76b3418 100644 --- a/homeassistant/components/airly/air_quality.py +++ b/homeassistant/components/airly/air_quality.py @@ -87,13 +87,13 @@ class AirlyAirQuality(CoordinatorEntity, AirQualityEntity): @round_state def particulate_matter_2_5(self): """Return the particulate matter 2.5 level.""" - return self.coordinator.data[ATTR_API_PM25] + return self.coordinator.data.get(ATTR_API_PM25) @property @round_state def particulate_matter_10(self): """Return the particulate matter 10 level.""" - return self.coordinator.data[ATTR_API_PM10] + return self.coordinator.data.get(ATTR_API_PM10) @property def attribution(self): @@ -120,12 +120,19 @@ class AirlyAirQuality(CoordinatorEntity, AirQualityEntity): @property def device_state_attributes(self): """Return the state attributes.""" - return { + attrs = { LABEL_AQI_DESCRIPTION: self.coordinator.data[ATTR_API_CAQI_DESCRIPTION], LABEL_ADVICE: self.coordinator.data[ATTR_API_ADVICE], LABEL_AQI_LEVEL: self.coordinator.data[ATTR_API_CAQI_LEVEL], - LABEL_PM_2_5_LIMIT: self.coordinator.data[ATTR_API_PM25_LIMIT], - LABEL_PM_2_5_PERCENT: round(self.coordinator.data[ATTR_API_PM25_PERCENT]), - LABEL_PM_10_LIMIT: self.coordinator.data[ATTR_API_PM10_LIMIT], - LABEL_PM_10_PERCENT: round(self.coordinator.data[ATTR_API_PM10_PERCENT]), } + if ATTR_API_PM25 in self.coordinator.data: + attrs[LABEL_PM_2_5_LIMIT] = self.coordinator.data[ATTR_API_PM25_LIMIT] + attrs[LABEL_PM_2_5_PERCENT] = round( + self.coordinator.data[ATTR_API_PM25_PERCENT] + ) + if ATTR_API_PM10 in self.coordinator.data: + attrs[LABEL_PM_10_LIMIT] = self.coordinator.data[ATTR_API_PM10_LIMIT] + attrs[LABEL_PM_10_PERCENT] = round( + self.coordinator.data[ATTR_API_PM10_PERCENT] + ) + return attrs diff --git a/homeassistant/components/airly/config_flow.py b/homeassistant/components/airly/config_flow.py index 58d6a4295e9..d7636d1db33 100644 --- a/homeassistant/components/airly/config_flow.py +++ b/homeassistant/components/airly/config_flow.py @@ -10,12 +10,17 @@ from homeassistant.const import ( CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, + HTTP_NOT_FOUND, HTTP_UNAUTHORIZED, ) from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from .const import DOMAIN, NO_AIRLY_SENSORS # pylint:disable=unused-import +from .const import ( # pylint:disable=unused-import + CONF_USE_NEAREST, + DOMAIN, + NO_AIRLY_SENSORS, +) class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -27,6 +32,7 @@ class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" errors = {} + use_nearest = False websession = async_get_clientsession(self.hass) @@ -36,23 +42,32 @@ class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) self._abort_if_unique_id_configured() try: - location_valid = await test_location( + location_point_valid = await test_location( websession, user_input["api_key"], user_input["latitude"], user_input["longitude"], ) + if not location_point_valid: + await test_location( + websession, + user_input["api_key"], + user_input["latitude"], + user_input["longitude"], + use_nearest=True, + ) except AirlyError as err: if err.status_code == HTTP_UNAUTHORIZED: errors["base"] = "invalid_api_key" - else: - if not location_valid: + if err.status_code == HTTP_NOT_FOUND: errors["base"] = "wrong_location" - - if not errors: - return self.async_create_entry( - title=user_input[CONF_NAME], data=user_input - ) + else: + if not location_point_valid: + use_nearest = True + return self.async_create_entry( + title=user_input[CONF_NAME], + data={**user_input, CONF_USE_NEAREST: use_nearest}, + ) return self.async_show_form( step_id="user", @@ -74,13 +89,17 @@ class AirlyFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) -async def test_location(client, api_key, latitude, longitude): +async def test_location(client, api_key, latitude, longitude, use_nearest=False): """Return true if location is valid.""" airly = Airly(api_key, client) - measurements = airly.create_measurements_session_point( - latitude=latitude, longitude=longitude - ) - + if use_nearest: + measurements = airly.create_measurements_session_nearest( + latitude=latitude, longitude=longitude, max_distance_km=5 + ) + else: + measurements = airly.create_measurements_session_point( + latitude=latitude, longitude=longitude + ) with async_timeout.timeout(10): await measurements.update() diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py index dc21d68a8d8..b4711b50dd2 100644 --- a/homeassistant/components/airly/const.py +++ b/homeassistant/components/airly/const.py @@ -13,6 +13,7 @@ ATTR_API_PM25_LIMIT = "PM25_LIMIT" ATTR_API_PM25_PERCENT = "PM25_PERCENT" ATTR_API_PRESSURE = "PRESSURE" ATTR_API_TEMPERATURE = "TEMPERATURE" +CONF_USE_NEAREST = "use_nearest" DEFAULT_NAME = "Airly" DOMAIN = "airly" MANUFACTURER = "Airly sp. z o.o." diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index d4f472dfca8..420d11a5963 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -67,7 +67,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): sensors = [] for sensor in SENSOR_TYPES: - sensors.append(AirlySensor(coordinator, name, sensor)) + # When we use the nearest method, we are not sure which sensors are available + if coordinator.data.get(sensor): + sensors.append(AirlySensor(coordinator, name, sensor)) async_add_entities(sensors, False) diff --git a/homeassistant/components/airly/translations/de.json b/homeassistant/components/airly/translations/de.json index 743a68a010e..8004444fdb9 100644 --- a/homeassistant/components/airly/translations/de.json +++ b/homeassistant/components/airly/translations/de.json @@ -1,9 +1,10 @@ { "config": { "abort": { - "already_configured": "Die Airly-Integration ist f\u00fcr diese Koordinaten bereits konfiguriert." + "already_configured": "Standort ist bereits konfiguriert" }, "error": { + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel", "wrong_location": "Keine Airly Luftmessstation an diesem Ort" }, "step": { @@ -12,7 +13,7 @@ "api_key": "API-Schl\u00fcssel", "latitude": "Breitengrad", "longitude": "L\u00e4ngengrad", - "name": "Name der Integration" + "name": "Name" }, "description": "Einrichtung der Airly-Luftqualit\u00e4t Integration. Um einen API-Schl\u00fcssel zu generieren, registriere dich auf https://developer.airly.eu/register", "title": "Airly" @@ -21,7 +22,7 @@ }, "system_health": { "info": { - "can_reach_server": "Airly Server erreichen" + "can_reach_server": "Airly-Server erreichen" } } } \ No newline at end of file diff --git a/homeassistant/components/airly/translations/tr.json b/homeassistant/components/airly/translations/tr.json index 1b6e9caa24c..144acc1e1ae 100644 --- a/homeassistant/components/airly/translations/tr.json +++ b/homeassistant/components/airly/translations/tr.json @@ -1,4 +1,21 @@ { + "config": { + "abort": { + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "latitude": "Enlem", + "longitude": "Boylam" + } + } + } + }, "system_health": { "info": { "can_reach_server": "Airly sunucusuna eri\u015fin" diff --git a/homeassistant/components/airly/translations/uk.json b/homeassistant/components/airly/translations/uk.json new file mode 100644 index 00000000000..51bcf5195df --- /dev/null +++ b/homeassistant/components/airly/translations/uk.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435." + }, + "error": { + "invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API", + "wrong_location": "\u0423 \u0446\u0456\u0439 \u043e\u0431\u043b\u0430\u0441\u0442\u0456 \u043d\u0435\u043c\u0430\u0454 \u0432\u0438\u043c\u0456\u0440\u044e\u0432\u0430\u043b\u044c\u043d\u0438\u0445 \u0441\u0442\u0430\u043d\u0446\u0456\u0439 Airly." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "description": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f \u0441\u0435\u0440\u0432\u0456\u0441\u0443 \u0437 \u0430\u043d\u0430\u043b\u0456\u0437\u0443 \u044f\u043a\u043e\u0441\u0442\u0456 \u043f\u043e\u0432\u0456\u0442\u0440\u044f Airly. \u0429\u043e\u0431 \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0437\u0430 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f\u043c https://developer.airly.eu/register.", + "title": "Airly" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Airly" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airnow/__init__.py b/homeassistant/components/airnow/__init__.py new file mode 100644 index 00000000000..5cbc87947f9 --- /dev/null +++ b/homeassistant/components/airnow/__init__.py @@ -0,0 +1,161 @@ +"""The AirNow integration.""" +import asyncio +import datetime +import logging + +from aiohttp.client_exceptions import ClientConnectorError +from pyairnow import WebServiceAPI +from pyairnow.conv import aqi_to_concentration +from pyairnow.errors import AirNowError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + ATTR_API_AQI, + ATTR_API_AQI_DESCRIPTION, + ATTR_API_AQI_LEVEL, + ATTR_API_AQI_PARAM, + ATTR_API_CAT_DESCRIPTION, + ATTR_API_CAT_LEVEL, + ATTR_API_CATEGORY, + ATTR_API_PM25, + ATTR_API_POLLUTANT, + ATTR_API_REPORT_DATE, + ATTR_API_REPORT_HOUR, + ATTR_API_STATE, + ATTR_API_STATION, + ATTR_API_STATION_LATITUDE, + ATTR_API_STATION_LONGITUDE, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) +PLATFORMS = ["sensor"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the AirNow component.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up AirNow from a config entry.""" + api_key = entry.data[CONF_API_KEY] + latitude = entry.data[CONF_LATITUDE] + longitude = entry.data[CONF_LONGITUDE] + distance = entry.data[CONF_RADIUS] + + # Reports are published hourly but update twice per hour + update_interval = datetime.timedelta(minutes=30) + + # Setup the Coordinator + session = async_get_clientsession(hass) + coordinator = AirNowDataUpdateCoordinator( + hass, session, api_key, latitude, longitude, distance, update_interval + ) + + # Sync with Coordinator + await coordinator.async_refresh() + if not coordinator.last_update_success: + raise ConfigEntryNotReady + + # Store Entity and Initialize Platforms + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = coordinator + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class AirNowDataUpdateCoordinator(DataUpdateCoordinator): + """Define an object to hold Airly data.""" + + def __init__( + self, hass, session, api_key, latitude, longitude, distance, update_interval + ): + """Initialize.""" + self.latitude = latitude + self.longitude = longitude + self.distance = distance + + self.airnow = WebServiceAPI(api_key, session=session) + + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) + + async def _async_update_data(self): + """Update data via library.""" + data = {} + try: + obs = await self.airnow.observations.latLong( + self.latitude, + self.longitude, + distance=self.distance, + ) + + except (AirNowError, ClientConnectorError) as error: + raise UpdateFailed(error) from error + + if not obs: + raise UpdateFailed("No data was returned from AirNow") + + max_aqi = 0 + max_aqi_level = 0 + max_aqi_desc = "" + max_aqi_poll = "" + for obv in obs: + # Convert AQIs to Concentration + pollutant = obv[ATTR_API_AQI_PARAM] + concentration = aqi_to_concentration(obv[ATTR_API_AQI], pollutant) + data[obv[ATTR_API_AQI_PARAM]] = concentration + + # Overall AQI is the max of all pollutant AQIs + if obv[ATTR_API_AQI] > max_aqi: + max_aqi = obv[ATTR_API_AQI] + max_aqi_level = obv[ATTR_API_CATEGORY][ATTR_API_CAT_LEVEL] + max_aqi_desc = obv[ATTR_API_CATEGORY][ATTR_API_CAT_DESCRIPTION] + max_aqi_poll = pollutant + + # Copy other data from PM2.5 Value + if obv[ATTR_API_AQI_PARAM] == ATTR_API_PM25: + # Copy Report Details + data[ATTR_API_REPORT_DATE] = obv[ATTR_API_REPORT_DATE] + data[ATTR_API_REPORT_HOUR] = obv[ATTR_API_REPORT_HOUR] + + # Copy Station Details + data[ATTR_API_STATE] = obv[ATTR_API_STATE] + data[ATTR_API_STATION] = obv[ATTR_API_STATION] + data[ATTR_API_STATION_LATITUDE] = obv[ATTR_API_STATION_LATITUDE] + data[ATTR_API_STATION_LONGITUDE] = obv[ATTR_API_STATION_LONGITUDE] + + # Store Overall AQI + data[ATTR_API_AQI] = max_aqi + data[ATTR_API_AQI_LEVEL] = max_aqi_level + data[ATTR_API_AQI_DESCRIPTION] = max_aqi_desc + data[ATTR_API_POLLUTANT] = max_aqi_poll + + return data diff --git a/homeassistant/components/airnow/config_flow.py b/homeassistant/components/airnow/config_flow.py new file mode 100644 index 00000000000..6d53ac133ee --- /dev/null +++ b/homeassistant/components/airnow/config_flow.py @@ -0,0 +1,110 @@ +"""Config flow for AirNow integration.""" +import logging + +from pyairnow import WebServiceAPI +from pyairnow.errors import AirNowError, InvalidKeyError +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS +from homeassistant.helpers.aiohttp_client import async_get_clientsession +import homeassistant.helpers.config_validation as cv + +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +async def validate_input(hass: core.HomeAssistant, data): + """ + Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + session = async_get_clientsession(hass) + client = WebServiceAPI(data[CONF_API_KEY], session=session) + + lat = data[CONF_LATITUDE] + lng = data[CONF_LONGITUDE] + distance = data[CONF_RADIUS] + + # Check that the provided latitude/longitude provide a response + try: + test_data = await client.observations.latLong(lat, lng, distance=distance) + + except InvalidKeyError as exc: + raise InvalidAuth from exc + except AirNowError as exc: + raise CannotConnect from exc + + if not test_data: + raise InvalidLocation + + # Validation Succeeded + return True + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for AirNow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + # Set a unique id based on latitude/longitude + await self.async_set_unique_id( + f"{user_input[CONF_LATITUDE]}-{user_input[CONF_LONGITUDE]}" + ) + self._abort_if_unique_id_configured() + + try: + # Validate inputs + await validate_input(self.hass, user_input) + + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except InvalidLocation: + errors["base"] = "invalid_location" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + # Create Entry + return self.async_create_entry( + title=f"AirNow Sensor at {user_input[CONF_LATITUDE]}, {user_input[CONF_LONGITUDE]}", + data=user_input, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_API_KEY): str, + vol.Optional( + CONF_LATITUDE, default=self.hass.config.latitude + ): cv.latitude, + vol.Optional( + CONF_LONGITUDE, default=self.hass.config.longitude + ): cv.longitude, + vol.Optional(CONF_RADIUS, default=150): int, + } + ), + errors=errors, + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" + + +class InvalidLocation(exceptions.HomeAssistantError): + """Error to indicate the location is invalid.""" diff --git a/homeassistant/components/airnow/const.py b/homeassistant/components/airnow/const.py new file mode 100644 index 00000000000..67a9289efc5 --- /dev/null +++ b/homeassistant/components/airnow/const.py @@ -0,0 +1,21 @@ +"""Constants for the AirNow integration.""" +ATTR_API_AQI = "AQI" +ATTR_API_AQI_LEVEL = "Category.Number" +ATTR_API_AQI_DESCRIPTION = "Category.Name" +ATTR_API_AQI_PARAM = "ParameterName" +ATTR_API_CATEGORY = "Category" +ATTR_API_CAT_LEVEL = "Number" +ATTR_API_CAT_DESCRIPTION = "Name" +ATTR_API_O3 = "O3" +ATTR_API_PM25 = "PM2.5" +ATTR_API_POLLUTANT = "Pollutant" +ATTR_API_REPORT_DATE = "HourObserved" +ATTR_API_REPORT_HOUR = "DateObserved" +ATTR_API_STATE = "StateCode" +ATTR_API_STATION = "ReportingArea" +ATTR_API_STATION_LATITUDE = "Latitude" +ATTR_API_STATION_LONGITUDE = "Longitude" +DEFAULT_NAME = "AirNow" +DOMAIN = "airnow" +SENSOR_AQI_ATTR_DESCR = "description" +SENSOR_AQI_ATTR_LEVEL = "level" diff --git a/homeassistant/components/airnow/manifest.json b/homeassistant/components/airnow/manifest.json new file mode 100644 index 00000000000..fee89ae4fff --- /dev/null +++ b/homeassistant/components/airnow/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "airnow", + "name": "AirNow", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/airnow", + "requirements": [ + "pyairnow==1.1.0" + ], + "codeowners": [ + "@asymworks" + ] +} diff --git a/homeassistant/components/airnow/sensor.py b/homeassistant/components/airnow/sensor.py new file mode 100644 index 00000000000..fed6def2b36 --- /dev/null +++ b/homeassistant/components/airnow/sensor.py @@ -0,0 +1,118 @@ +"""Support for the AirNow sensor service.""" +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_DEVICE_CLASS, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + CONCENTRATION_PARTS_PER_MILLION, +) +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ( + ATTR_API_AQI, + ATTR_API_AQI_DESCRIPTION, + ATTR_API_AQI_LEVEL, + ATTR_API_O3, + ATTR_API_PM25, + DOMAIN, + SENSOR_AQI_ATTR_DESCR, + SENSOR_AQI_ATTR_LEVEL, +) + +ATTRIBUTION = "Data provided by AirNow" + +ATTR_ICON = "icon" +ATTR_LABEL = "label" +ATTR_UNIT = "unit" + +PARALLEL_UPDATES = 1 + +SENSOR_TYPES = { + ATTR_API_AQI: { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:blur", + ATTR_LABEL: ATTR_API_AQI, + ATTR_UNIT: "aqi", + }, + ATTR_API_PM25: { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:blur", + ATTR_LABEL: ATTR_API_PM25, + ATTR_UNIT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + }, + ATTR_API_O3: { + ATTR_DEVICE_CLASS: None, + ATTR_ICON: "mdi:blur", + ATTR_LABEL: ATTR_API_O3, + ATTR_UNIT: CONCENTRATION_PARTS_PER_MILLION, + }, +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up AirNow sensor entities based on a config entry.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id] + + sensors = [] + for sensor in SENSOR_TYPES: + sensors.append(AirNowSensor(coordinator, sensor)) + + async_add_entities(sensors, False) + + +class AirNowSensor(CoordinatorEntity): + """Define an AirNow sensor.""" + + def __init__(self, coordinator, kind): + """Initialize.""" + super().__init__(coordinator) + self.kind = kind + self._device_class = None + self._state = None + self._icon = None + self._unit_of_measurement = None + self._attrs = {ATTR_ATTRIBUTION: ATTRIBUTION} + + @property + def name(self): + """Return the name.""" + return f"AirNow {SENSOR_TYPES[self.kind][ATTR_LABEL]}" + + @property + def state(self): + """Return the state.""" + self._state = self.coordinator.data[self.kind] + return self._state + + @property + def device_state_attributes(self): + """Return the state attributes.""" + if self.kind == ATTR_API_AQI: + self._attrs[SENSOR_AQI_ATTR_DESCR] = self.coordinator.data[ + ATTR_API_AQI_DESCRIPTION + ] + self._attrs[SENSOR_AQI_ATTR_LEVEL] = self.coordinator.data[ + ATTR_API_AQI_LEVEL + ] + + return self._attrs + + @property + def icon(self): + """Return the icon.""" + self._icon = SENSOR_TYPES[self.kind][ATTR_ICON] + return self._icon + + @property + def device_class(self): + """Return the device_class.""" + return SENSOR_TYPES[self.kind][ATTR_DEVICE_CLASS] + + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return f"{self.coordinator.latitude}-{self.coordinator.longitude}-{self.kind.lower()}" + + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return SENSOR_TYPES[self.kind][ATTR_UNIT] diff --git a/homeassistant/components/airnow/strings.json b/homeassistant/components/airnow/strings.json new file mode 100644 index 00000000000..a73ad6d179c --- /dev/null +++ b/homeassistant/components/airnow/strings.json @@ -0,0 +1,26 @@ +{ + "title": "AirNow", + "config": { + "step": { + "user": { + "title": "AirNow", + "description": "Set up AirNow air quality integration. To generate API key go to https://docs.airnowapi.org/account/request/", + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]", + "latitude": "[%key:common::config_flow::data::latitude%]", + "longitude": "[%key:common::config_flow::data::longitude%]", + "radius": "Station Radius (miles; optional)" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "invalid_location": "No results found for that location", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/airnow/translations/ca.json b/homeassistant/components/airnow/translations/ca.json new file mode 100644 index 00000000000..2db3cfad563 --- /dev/null +++ b/homeassistant/components/airnow/translations/ca.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "invalid_location": "No s'ha trobat cap resultat per a aquesta ubicaci\u00f3", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API", + "latitude": "Latitud", + "longitude": "Longitud", + "radius": "Radi de l'estaci\u00f3 (milles; opcional)" + }, + "description": "Configura la integraci\u00f3 de qualitat d'aire AirNow. Per generar la clau API, v\u00e9s a https://docs.airnowapi.org/account/request/", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/cs.json b/homeassistant/components/airnow/translations/cs.json new file mode 100644 index 00000000000..d978e44c70a --- /dev/null +++ b/homeassistant/components/airnow/translations/cs.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "api_key": "Kl\u00ed\u010d API", + "latitude": "Zem\u011bpisn\u00e1 \u0161\u00ed\u0159ka", + "longitude": "Zem\u011bpisn\u00e1 d\u00e9lka" + }, + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/de.json b/homeassistant/components/airnow/translations/de.json new file mode 100644 index 00000000000..c98fc6d7415 --- /dev/null +++ b/homeassistant/components/airnow/translations/de.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "invalid_location": "F\u00fcr diesen Standort wurden keine Ergebnisse gefunden", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "api_key": "API-Schl\u00fcssel", + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad" + }, + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/en.json b/homeassistant/components/airnow/translations/en.json new file mode 100644 index 00000000000..371bb270ac1 --- /dev/null +++ b/homeassistant/components/airnow/translations/en.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "invalid_location": "No results found for that location", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "latitude": "Latitude", + "longitude": "Longitude", + "radius": "Station Radius (miles; optional)" + }, + "description": "Set up AirNow air quality integration. To generate API key go to https://docs.airnowapi.org/account/request/", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/es.json b/homeassistant/components/airnow/translations/es.json new file mode 100644 index 00000000000..d6a228a6e27 --- /dev/null +++ b/homeassistant/components/airnow/translations/es.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "invalid_location": "No se han encontrado resultados para esa ubicaci\u00f3n", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "Clave API", + "latitude": "Latitud", + "longitude": "Longitud", + "radius": "Radio de la estaci\u00f3n (millas; opcional)" + }, + "description": "Configurar la integraci\u00f3n de calidad del aire de AirNow. Para generar una clave API, ve a https://docs.airnowapi.org/account/request/", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/et.json b/homeassistant/components/airnow/translations/et.json new file mode 100644 index 00000000000..52b2bb618e0 --- /dev/null +++ b/homeassistant/components/airnow/translations/et.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Vigane autentimine", + "invalid_location": "Selle asukoha jaoks ei leitud andmeid", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "api_key": "API v\u00f5ti", + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad", + "radius": "Jaama raadius (miilid; valikuline)" + }, + "description": "Seadista AirNow \u00f5hukvaliteedi sidumine. API-v\u00f5tme loomiseks mine aadressile https://docs.airnowapi.org/account/request/", + "title": "" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/fr.json b/homeassistant/components/airnow/translations/fr.json new file mode 100644 index 00000000000..ff85d9318e9 --- /dev/null +++ b/homeassistant/components/airnow/translations/fr.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec \u00e0 la connexion", + "invalid_auth": "Authentification invalide", + "invalid_location": "Aucun r\u00e9sultat trouv\u00e9 pour cet emplacement", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "api_key": "Cl\u00e9 API", + "latitude": "Latitude", + "longitude": "Longitude", + "radius": "Rayon d'action de la station (en miles, facultatif)" + }, + "description": "Configurez l'int\u00e9gration de la qualit\u00e9 de l'air AirNow. Pour g\u00e9n\u00e9rer la cl\u00e9 API, acc\u00e9dez \u00e0 https://docs.airnowapi.org/account/request/", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/it.json b/homeassistant/components/airnow/translations/it.json new file mode 100644 index 00000000000..9dda15dfbd2 --- /dev/null +++ b/homeassistant/components/airnow/translations/it.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "invalid_location": "Nessun risultato trovato per quella localit\u00e0", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API", + "latitude": "Latitudine", + "longitude": "Logitudine", + "radius": "Raggio stazione (miglia; opzionale)" + }, + "description": "Configura l'integrazione per la qualit\u00e0 dell'aria AirNow. Per generare la chiave API, vai su https://docs.airnowapi.org/account/request/", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/lb.json b/homeassistant/components/airnow/translations/lb.json new file mode 100644 index 00000000000..a62bd0bf478 --- /dev/null +++ b/homeassistant/components/airnow/translations/lb.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "invalid_location": "Keng Resultater fonnt fir d\u00ebse Standuert", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "api_key": "API Schl\u00ebssel", + "latitude": "L\u00e4ngegrad", + "longitude": "Breedegrag" + }, + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/no.json b/homeassistant/components/airnow/translations/no.json new file mode 100644 index 00000000000..19fa7e12207 --- /dev/null +++ b/homeassistant/components/airnow/translations/no.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "invalid_location": "Ingen resultater funnet for den plasseringen", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkel", + "latitude": "Breddegrad", + "longitude": "Lengdegrad", + "radius": "Stasjonsradius (miles; valgfritt)" + }, + "description": "Konfigurer integrering av luftkvalitet i AirNow. For \u00e5 generere en API-n\u00f8kkel, g\u00e5r du til https://docs.airnowapi.org/account/request/", + "title": "" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/pl.json b/homeassistant/components/airnow/translations/pl.json new file mode 100644 index 00000000000..fe4310607b9 --- /dev/null +++ b/homeassistant/components/airnow/translations/pl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "invalid_location": "Brak wynik\u00f3w dla tej lokalizacji", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API", + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "radius": "Promie\u0144 od stacji (w milach; opcjonalnie)" + }, + "description": "Konfiguracja integracji jako\u015bci powietrza AirNow. Aby wygenerowa\u0107 klucz API, przejd\u017a do https://docs.airnowapi.org/account/request/", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/pt.json b/homeassistant/components/airnow/translations/pt.json new file mode 100644 index 00000000000..3aa509dd6e8 --- /dev/null +++ b/homeassistant/components/airnow/translations/pt.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "unknown": "Erro inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "API Key", + "latitude": "Latitude", + "longitude": "Longitude" + }, + "title": "" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/ru.json b/homeassistant/components/airnow/translations/ru.json new file mode 100644 index 00000000000..650633cc816 --- /dev/null +++ b/homeassistant/components/airnow/translations/ru.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_location": "\u0414\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u043e\u0432 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", + "radius": "\u0420\u0430\u0434\u0438\u0443\u0441 \u0441\u0442\u0430\u043d\u0446\u0438\u0438 (\u0432 \u043c\u0438\u043b\u044f\u0445; \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)" + }, + "description": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u043f\u043e \u0430\u043d\u0430\u043b\u0438\u0437\u0443 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u0430 \u0432\u043e\u0437\u0434\u0443\u0445\u0430 AirNow. \u0427\u0442\u043e\u0431\u044b \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043f\u043e \u0441\u0441\u044b\u043b\u043a\u0435 https://docs.airnowapi.org/account/request/.", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/tr.json b/homeassistant/components/airnow/translations/tr.json new file mode 100644 index 00000000000..06af714dc87 --- /dev/null +++ b/homeassistant/components/airnow/translations/tr.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_location": "Bu konum i\u00e7in hi\u00e7bir sonu\u00e7 bulunamad\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "latitude": "Enlem", + "longitude": "Boylam", + "radius": "\u0130stasyon Yar\u0131\u00e7ap\u0131 (mil; iste\u011fe ba\u011fl\u0131)" + }, + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/uk.json b/homeassistant/components/airnow/translations/uk.json new file mode 100644 index 00000000000..bb872123f54 --- /dev/null +++ b/homeassistant/components/airnow/translations/uk.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "invalid_location": "\u041d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0456\u0432 \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", + "radius": "\u0420\u0430\u0434\u0456\u0443\u0441 \u0441\u0442\u0430\u043d\u0446\u0456\u0457 (\u043c\u0438\u043b\u0456; \u043d\u0435\u043e\u0431\u043e\u0432\u2019\u044f\u0437\u043a\u043e\u0432\u043e)" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e \u044f\u043a\u043e\u0441\u0442\u0456 \u043f\u043e\u0432\u0456\u0442\u0440\u044f AirNow. \u0429\u043e\u0431 \u0437\u0433\u0435\u043d\u0435\u0440\u0443\u0432\u0430\u0442\u0438 \u043a\u043b\u044e\u0447 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u043d\u0430 \u0441\u0442\u043e\u0440\u0456\u043d\u043a\u0443 https://docs.airnowapi.org/account/request/", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airnow/translations/zh-Hant.json b/homeassistant/components/airnow/translations/zh-Hant.json new file mode 100644 index 00000000000..0f6008e75a6 --- /dev/null +++ b/homeassistant/components/airnow/translations/zh-Hant.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_location": "\u627e\u4e0d\u5230\u8a72\u4f4d\u7f6e\u7684\u7d50\u679c", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "api_key": "API \u5bc6\u9470", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6", + "radius": "\u89c0\u6e2c\u7ad9\u534a\u5f91\uff08\u82f1\u91cc\uff1b\u9078\u9805\uff09" + }, + "description": "\u6b32\u8a2d\u5b9a AirNow \u7a7a\u6c23\u54c1\u8cea\u6574\u5408\u3002\u8acb\u81f3 https://docs.airnowapi.org/account/request/ \u7522\u751f API \u5bc6\u9470", + "title": "AirNow" + } + } + }, + "title": "AirNow" +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index a81c118ecc9..49a05272488 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -58,25 +58,6 @@ NODE_PRO_SENSORS = [ (SENSOR_KIND_TEMPERATURE, "Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS), ] -POLLUTANT_LEVEL_MAPPING = [ - {"label": "Good", "icon": "mdi:emoticon-excited", "minimum": 0, "maximum": 50}, - {"label": "Moderate", "icon": "mdi:emoticon-happy", "minimum": 51, "maximum": 100}, - { - "label": "Unhealthy for sensitive groups", - "icon": "mdi:emoticon-neutral", - "minimum": 101, - "maximum": 150, - }, - {"label": "Unhealthy", "icon": "mdi:emoticon-sad", "minimum": 151, "maximum": 200}, - { - "label": "Very Unhealthy", - "icon": "mdi:emoticon-dead", - "minimum": 201, - "maximum": 300, - }, - {"label": "Hazardous", "icon": "mdi:biohazard", "minimum": 301, "maximum": 10000}, -] - POLLUTANT_MAPPING = { "co": {"label": "Carbon Monoxide", "unit": CONCENTRATION_PARTS_PER_MILLION}, "n2": {"label": "Nitrogen Dioxide", "unit": CONCENTRATION_PARTS_PER_BILLION}, @@ -87,6 +68,22 @@ POLLUTANT_MAPPING = { } +@callback +def async_get_pollutant_level_info(value): + """Return a verbal pollutant level (and associated icon) for a numeric value.""" + if 0 <= value <= 50: + return ("Good", "mdi:emoticon-excited") + if 51 <= value <= 100: + return ("Moderate", "mdi:emoticon-happy") + if 101 <= value <= 150: + return ("Unhealthy for sensitive groups", "mdi:emoticon-neutral") + if 151 <= value <= 200: + return ("Unhealthy", "mdi:emoticon-sad") + if 201 <= value <= 300: + return ("Very Unhealthy", "mdi:emoticon-dead") + return ("Hazardous", "mdi:biohazard") + + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up AirVisual sensors based on a config entry.""" coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] @@ -171,13 +168,7 @@ class AirVisualGeographySensor(AirVisualEntity): if self._kind == SENSOR_KIND_LEVEL: aqi = data[f"aqi{self._locale}"] - [level] = [ - i - for i in POLLUTANT_LEVEL_MAPPING - if i["minimum"] <= aqi <= i["maximum"] - ] - self._state = level["label"] - self._icon = level["icon"] + self._state, self._icon = async_get_pollutant_level_info(aqi) elif self._kind == SENSOR_KIND_AQI: self._state = data[f"aqi{self._locale}"] elif self._kind == SENSOR_KIND_POLLUTANT: diff --git a/homeassistant/components/airvisual/translations/ar.json b/homeassistant/components/airvisual/translations/ar.json new file mode 100644 index 00000000000..771d88e8434 --- /dev/null +++ b/homeassistant/components/airvisual/translations/ar.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "geography_by_name": { + "data": { + "country": "\u0627\u0644\u062f\u0648\u0644\u0629" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/ca.json b/homeassistant/components/airvisual/translations/ca.json index d7a0ec2bd99..29df3dc7ca2 100644 --- a/homeassistant/components/airvisual/translations/ca.json +++ b/homeassistant/components/airvisual/translations/ca.json @@ -7,7 +7,8 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3", "general_error": "Error inesperat", - "invalid_api_key": "Clau API inv\u00e0lida" + "invalid_api_key": "Clau API inv\u00e0lida", + "location_not_found": "No s'ha trobat la ubicaci\u00f3" }, "step": { "geography": { @@ -17,7 +18,26 @@ "longitude": "Longitud" }, "description": "Utilitza l'API d'AirVisual per monitoritzar una ubicaci\u00f3 geogr\u00e0fica.", - "title": "Configuraci\u00f3 localitzaci\u00f3 geogr\u00e0fica" + "title": "Configura una ubicaci\u00f3 geogr\u00e0fica" + }, + "geography_by_coords": { + "data": { + "api_key": "Clau API", + "latitude": "Latitud", + "longitude": "Longitud" + }, + "description": "Utilitza l'API d'AirVisual per monitoritzar una latitud/longitud.", + "title": "Configura una ubicaci\u00f3 geogr\u00e0fica" + }, + "geography_by_name": { + "data": { + "api_key": "Clau API", + "city": "Ciutat", + "country": "Pa\u00eds", + "state": "Estat" + }, + "description": "Utilitza l'API d'AirVisual per monitoritzar un/a ciutat/estat/pa\u00eds", + "title": "Configura una ubicaci\u00f3 geogr\u00e0fica" }, "node_pro": { "data": { diff --git a/homeassistant/components/airvisual/translations/de.json b/homeassistant/components/airvisual/translations/de.json index 63012e23da1..a16b02915ee 100644 --- a/homeassistant/components/airvisual/translations/de.json +++ b/homeassistant/components/airvisual/translations/de.json @@ -1,12 +1,13 @@ { "config": { "abort": { - "already_configured": "Diese Koordinaten oder Node/Pro ID sind bereits registriert." + "already_configured": "Diese Koordinaten oder Node/Pro ID sind bereits registriert.", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { - "cannot_connect": "Verbindungsfehler", - "general_error": "Es gab einen unbekannten Fehler.", - "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel bereitgestellt." + "cannot_connect": "Verbindung fehlgeschlagen", + "general_error": "Unerwarteter Fehler", + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel" }, "step": { "geography": { @@ -19,7 +20,7 @@ }, "node_pro": { "data": { - "ip_address": "IP-Adresse/Hostname des Ger\u00e4ts", + "ip_address": "Host", "password": "Passwort" }, "description": "\u00dcberwachen Sie eine pers\u00f6nliche AirVisual-Einheit. Das Passwort kann von der Benutzeroberfl\u00e4che des Ger\u00e4ts abgerufen werden.", diff --git a/homeassistant/components/airvisual/translations/en.json b/homeassistant/components/airvisual/translations/en.json index 129abcc29e5..1a52bfb7e3b 100644 --- a/homeassistant/components/airvisual/translations/en.json +++ b/homeassistant/components/airvisual/translations/en.json @@ -19,6 +19,15 @@ "description": "Use the AirVisual cloud API to monitor a geographical location.", "title": "Configure a Geography" }, + "geography_by_coords": { + "data": { + "api_key": "API Key", + "latitude": "Latitude", + "longitude": "Longitude" + }, + "description": "Use the AirVisual cloud API to monitor a geographical location.", + "title": "Configure a Geography" + }, "node_pro": { "data": { "ip_address": "Host", @@ -54,4 +63,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/airvisual/translations/et.json b/homeassistant/components/airvisual/translations/et.json index 4bbf04817f9..9912dbce035 100644 --- a/homeassistant/components/airvisual/translations/et.json +++ b/homeassistant/components/airvisual/translations/et.json @@ -7,7 +7,8 @@ "error": { "cannot_connect": "\u00dchendamine nurjus", "general_error": "Tundmatu viga", - "invalid_api_key": "Vale API v\u00f5ti" + "invalid_api_key": "Vale API v\u00f5ti", + "location_not_found": "Asukohta ei leitud" }, "step": { "geography": { @@ -19,6 +20,25 @@ "description": "Kasutage AirVisual pilve API-t geograafilise asukoha j\u00e4lgimiseks.", "title": "Seadista Geography" }, + "geography_by_coords": { + "data": { + "api_key": "API v\u00f5ti", + "latitude": "Laiuskraad", + "longitude": "Pikkuskraad" + }, + "description": "Kasuta AirVisual pilve API-t pikkus/laiuskraadi j\u00e4lgimiseks.", + "title": "Seadista Geography sidumine" + }, + "geography_by_name": { + "data": { + "api_key": "API v\u00f5ti", + "city": "Linn", + "country": "Riik", + "state": "olek" + }, + "description": "Kasuta AirVisual pilve API-t linna/osariigi/riigi j\u00e4lgimiseks.", + "title": "Seadista Geography sidumine" + }, "node_pro": { "data": { "ip_address": "\u00dcksuse IP-aadress / hostinimi", diff --git a/homeassistant/components/airvisual/translations/fr.json b/homeassistant/components/airvisual/translations/fr.json index 90857d826ea..d1a0d3d511a 100644 --- a/homeassistant/components/airvisual/translations/fr.json +++ b/homeassistant/components/airvisual/translations/fr.json @@ -6,8 +6,8 @@ }, "error": { "cannot_connect": "\u00c9chec de connexion", - "general_error": "Une erreur inconnue est survenue.", - "invalid_api_key": "La cl\u00e9 API fournie n'est pas valide." + "general_error": "Erreur inattendue", + "invalid_api_key": "Cl\u00e9 API invalide" }, "step": { "geography": { @@ -21,11 +21,11 @@ }, "node_pro": { "data": { - "ip_address": "Adresse IP / nom d'h\u00f4te de l'unit\u00e9", + "ip_address": "H\u00f4te", "password": "Mot de passe" }, - "description": "Surveillez une unit\u00e9 AirVisual personnelle. Le mot de passe peut \u00eatre r\u00e9cup\u00e9r\u00e9 dans l'interface utilisateur de l'unit\u00e9.", - "title": "Configurer un AirVisual Node/Pro" + "description": "Surveillez une unit\u00e9 personnelle AirVisual. Le mot de passe peut \u00eatre r\u00e9cup\u00e9r\u00e9 dans l'interface utilisateur de l'unit\u00e9.", + "title": "Configurer un noeud AirVisual Pro" }, "reauth_confirm": { "data": { diff --git a/homeassistant/components/airvisual/translations/no.json b/homeassistant/components/airvisual/translations/no.json index abf4a9f62e4..7c5b0333652 100644 --- a/homeassistant/components/airvisual/translations/no.json +++ b/homeassistant/components/airvisual/translations/no.json @@ -7,7 +7,8 @@ "error": { "cannot_connect": "Tilkobling mislyktes", "general_error": "Uventet feil", - "invalid_api_key": "Ugyldig API-n\u00f8kkel" + "invalid_api_key": "Ugyldig API-n\u00f8kkel", + "location_not_found": "Stedet ble ikke funnet" }, "step": { "geography": { @@ -19,6 +20,25 @@ "description": "Bruk AirVisual cloud API til \u00e5 overv\u00e5ke en geografisk plassering.", "title": "Konfigurer en Geography" }, + "geography_by_coords": { + "data": { + "api_key": "API-n\u00f8kkel", + "latitude": "Breddegrad", + "longitude": "Lengdegrad" + }, + "description": "Bruk AirVisual cloud API til \u00e5 overv\u00e5ke en breddegrad/lengdegrad.", + "title": "Konfigurer en Geography" + }, + "geography_by_name": { + "data": { + "api_key": "API-n\u00f8kkel", + "city": "By", + "country": "Land", + "state": "stat" + }, + "description": "Bruk AirVisual cloud API til \u00e5 overv\u00e5ke en by/stat/land.", + "title": "Konfigurer en Geography" + }, "node_pro": { "data": { "ip_address": "Vert", diff --git a/homeassistant/components/airvisual/translations/pl.json b/homeassistant/components/airvisual/translations/pl.json index 10af1fc2ee0..5590a951641 100644 --- a/homeassistant/components/airvisual/translations/pl.json +++ b/homeassistant/components/airvisual/translations/pl.json @@ -7,7 +7,8 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "general_error": "Nieoczekiwany b\u0142\u0105d", - "invalid_api_key": "Nieprawid\u0142owy klucz API" + "invalid_api_key": "Nieprawid\u0142owy klucz API", + "location_not_found": "Nie znaleziono lokalizacji" }, "step": { "geography": { @@ -19,6 +20,25 @@ "description": "U\u017cyj interfejsu API chmury AirVisual do monitorowania lokalizacji geograficznej.", "title": "Konfiguracja Geography" }, + "geography_by_coords": { + "data": { + "api_key": "Klucz API", + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna" + }, + "description": "U\u017cyj API chmury AirVisual do monitorowania szeroko\u015bci/d\u0142ugo\u015bci geograficznej.", + "title": "Konfiguracja Geography" + }, + "geography_by_name": { + "data": { + "api_key": "Klucz API", + "city": "Miasto", + "country": "Kraj", + "state": "Stan" + }, + "description": "U\u017cyj API chmury AirVisual do monitorowania miasta/stanu/kraju.", + "title": "Konfiguracja Geography" + }, "node_pro": { "data": { "ip_address": "Nazwa hosta lub adres IP", diff --git a/homeassistant/components/airvisual/translations/sv.json b/homeassistant/components/airvisual/translations/sv.json index 4c4e1271d72..f375b4fc598 100644 --- a/homeassistant/components/airvisual/translations/sv.json +++ b/homeassistant/components/airvisual/translations/sv.json @@ -1,7 +1,8 @@ { "config": { "error": { - "general_error": "Ett ok\u00e4nt fel intr\u00e4ffade." + "general_error": "Ett ok\u00e4nt fel intr\u00e4ffade.", + "invalid_api_key": "Ogiltig API-nyckel" }, "step": { "geography": { diff --git a/homeassistant/components/airvisual/translations/tr.json b/homeassistant/components/airvisual/translations/tr.json new file mode 100644 index 00000000000..3d20c8ea9fc --- /dev/null +++ b/homeassistant/components/airvisual/translations/tr.json @@ -0,0 +1,59 @@ +{ + "config": { + "abort": { + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "general_error": "Beklenmeyen hata", + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131", + "location_not_found": "Konum bulunamad\u0131" + }, + "step": { + "geography": { + "data": { + "api_key": "API Anahtar\u0131", + "latitude": "Enlem", + "longitude": "Boylam" + } + }, + "geography_by_coords": { + "data": { + "api_key": "API Anahtar\u0131", + "latitude": "Enlem", + "longitude": "Boylam" + }, + "description": "Bir enlem / boylam\u0131 izlemek i\u00e7in AirVisual bulut API'sini kullan\u0131n.", + "title": "Bir Co\u011frafyay\u0131 Yap\u0131land\u0131rma" + }, + "geography_by_name": { + "data": { + "api_key": "API Anahtar\u0131", + "city": "\u015eehir", + "country": "\u00dclke", + "state": "durum" + }, + "title": "Bir Co\u011frafyay\u0131 Yap\u0131land\u0131rma" + }, + "node_pro": { + "data": { + "ip_address": "Ana Bilgisayar", + "password": "Parola" + }, + "description": "Ki\u015fisel bir AirVisual \u00fcnitesini izleyin. Parola, \u00fcnitenin kullan\u0131c\u0131 aray\u00fcz\u00fcnden al\u0131nabilir." + }, + "reauth_confirm": { + "data": { + "api_key": "API Anahtar\u0131" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "AirVisual'\u0131 yap\u0131land\u0131r\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/uk.json b/homeassistant/components/airvisual/translations/uk.json new file mode 100644 index 00000000000..d99c58de7c0 --- /dev/null +++ b/homeassistant/components/airvisual/translations/uk.json @@ -0,0 +1,57 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435. \u0410\u0431\u043e \u0446\u0435\u0439 Node / Pro ID \u0432\u0436\u0435 \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u043e\u0432\u0430\u043d\u0438\u0439.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "general_error": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430", + "invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API" + }, + "step": { + "geography": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430" + }, + "description": "\u041c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433 \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u0445\u043c\u0430\u0440\u043d\u043e\u0433\u043e API AirVisual.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f" + }, + "node_pro": { + "data": { + "ip_address": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e AirVisual. \u041f\u0430\u0440\u043e\u043b\u044c \u043c\u043e\u0436\u043d\u0430 \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0432 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0456 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AirVisual Node / Pro" + }, + "reauth_confirm": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0444\u0456\u043b\u044e" + }, + "user": { + "data": { + "cloud_api": "\u041c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f", + "node_pro": "AirVisual Node Pro", + "type": "\u0422\u0438\u043f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0442\u0438\u043f \u0434\u0430\u043d\u0438\u0445 AirVisual, \u044f\u043a\u0438\u0439 \u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0432\u0456\u0434\u0441\u0442\u0435\u0436\u0443\u0432\u0430\u0442\u0438.", + "title": "AirVisual" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u0443\u0432\u0430\u0442\u0438 \u0432\u0456\u0434\u0441\u0442\u0435\u0436\u0443\u0432\u0430\u043d\u0443 \u043e\u0431\u043b\u0430\u0441\u0442\u044c \u043d\u0430 \u043a\u0430\u0440\u0442\u0456" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AirVisual" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/translations/zh-Hant.json b/homeassistant/components/airvisual/translations/zh-Hant.json index 4bdc2959047..3767d41b519 100644 --- a/homeassistant/components/airvisual/translations/zh-Hant.json +++ b/homeassistant/components/airvisual/translations/zh-Hant.json @@ -7,7 +7,8 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557", "general_error": "\u672a\u9810\u671f\u932f\u8aa4", - "invalid_api_key": "API \u5bc6\u9470\u7121\u6548" + "invalid_api_key": "API \u5bc6\u9470\u7121\u6548", + "location_not_found": "\u627e\u4e0d\u5230\u5730\u9ede" }, "step": { "geography": { @@ -19,6 +20,25 @@ "description": "\u4f7f\u7528 AirVisual \u96f2\u7aef API \u4ee5\u76e3\u63a7\u5730\u7406\u5ea7\u6a19\u3002", "title": "\u8a2d\u5b9a\u5730\u7406\u5ea7\u6a19" }, + "geography_by_coords": { + "data": { + "api_key": "API \u5bc6\u9470", + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6" + }, + "description": "\u4f7f\u7528 AirVisual \u96f2\u7aef API \u4ee5\u76e3\u63a7\u7d93\u5ea6/\u7def\u5ea6\u3002", + "title": "\u8a2d\u5b9a\u5730\u7406\u5ea7\u6a19" + }, + "geography_by_name": { + "data": { + "api_key": "API \u5bc6\u9470", + "city": "\u57ce\u5e02", + "country": "\u570b\u5bb6", + "state": "\u5dde" + }, + "description": "\u4f7f\u7528 AirVisual \u96f2\u7aef API \u4ee5\u76e3\u63a7\u57ce\u5e02/\u5dde/\u570b\u5bb6\u3002", + "title": "\u8a2d\u5b9a\u5730\u7406\u5ea7\u6a19" + }, "node_pro": { "data": { "ip_address": "\u4e3b\u6a5f\u7aef", diff --git a/homeassistant/components/alarm_control_panel/device_trigger.py b/homeassistant/components/alarm_control_panel/device_trigger.py index cb07ff35e96..bb5d82c52b1 100644 --- a/homeassistant/components/alarm_control_panel/device_trigger.py +++ b/homeassistant/components/alarm_control_panel/device_trigger.py @@ -23,7 +23,6 @@ from homeassistant.const import ( STATE_ALARM_ARMED_NIGHT, STATE_ALARM_ARMING, STATE_ALARM_DISARMED, - STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, ) from homeassistant.core import CALLBACK_TYPE, HomeAssistant @@ -143,13 +142,10 @@ async def async_attach_trigger( from_state = STATE_ALARM_DISARMED to_state = STATE_ALARM_ARMING elif config[CONF_TYPE] == "armed_home": - from_state = STATE_ALARM_PENDING or STATE_ALARM_ARMING to_state = STATE_ALARM_ARMED_HOME elif config[CONF_TYPE] == "armed_away": - from_state = STATE_ALARM_PENDING or STATE_ALARM_ARMING to_state = STATE_ALARM_ARMED_AWAY elif config[CONF_TYPE] == "armed_night": - from_state = STATE_ALARM_PENDING or STATE_ALARM_ARMING to_state = STATE_ALARM_ARMED_NIGHT state_config = { diff --git a/homeassistant/components/alarm_control_panel/translations/tr.json b/homeassistant/components/alarm_control_panel/translations/tr.json index ebbcf568338..cc509430436 100644 --- a/homeassistant/components/alarm_control_panel/translations/tr.json +++ b/homeassistant/components/alarm_control_panel/translations/tr.json @@ -1,4 +1,10 @@ { + "device_automation": { + "trigger_type": { + "disarmed": "{entity_name} b\u0131rak\u0131ld\u0131", + "triggered": "{entity_name} tetiklendi" + } + }, "state": { "_": { "armed": "Etkin", diff --git a/homeassistant/components/alarm_control_panel/translations/uk.json b/homeassistant/components/alarm_control_panel/translations/uk.json index e618e297019..b50fd9f459d 100644 --- a/homeassistant/components/alarm_control_panel/translations/uk.json +++ b/homeassistant/components/alarm_control_panel/translations/uk.json @@ -1,13 +1,36 @@ { + "device_automation": { + "action_type": { + "arm_away": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u041d\u0435 \u0432\u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "arm_home": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u0412\u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "arm_night": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u041d\u0456\u0447\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "disarm": "\u0412\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u043e\u0445\u043e\u0440\u043e\u043d\u0443 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "trigger": "{entity_name} \u0441\u043f\u0440\u0430\u0446\u044c\u043e\u0432\u0443\u0454" + }, + "condition_type": { + "is_armed_away": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u041d\u0435 \u0432\u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "is_armed_home": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u0412\u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "is_armed_night": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u041d\u0456\u0447\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "is_disarmed": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0430 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "is_triggered": "{entity_name} \u0441\u043f\u0440\u0430\u0446\u044c\u043e\u0432\u0443\u0454" + }, + "trigger_type": { + "armed_away": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u041d\u0435 \u0432\u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "armed_home": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u0412\u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "armed_night": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \"\u041d\u0456\u0447\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "disarmed": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0430 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 {entity_name}", + "triggered": "{entity_name} \u0441\u043f\u0440\u0430\u0446\u044c\u043e\u0432\u0443\u0454" + } + }, "state": { "_": { "armed": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430", - "armed_away": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 (\u043d\u0435 \u0432\u0434\u043e\u043c\u0430)", + "armed_away": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 (\u041d\u0435 \u0432\u0434\u043e\u043c\u0430)", "armed_custom_bypass": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 \u0437 \u0432\u0438\u043d\u044f\u0442\u043a\u0430\u043c\u0438", - "armed_home": "\u0411\u0443\u0434\u0438\u043d\u043a\u043e\u0432\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0430", + "armed_home": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 (\u0412\u0434\u043e\u043c\u0430)", "armed_night": "\u041d\u0456\u0447\u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0430", "arming": "\u0421\u0442\u0430\u0432\u043b\u044e \u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0443", - "disarmed": "\u0417\u043d\u044f\u0442\u043e", + "disarmed": "\u0417\u043d\u044f\u0442\u043e \u0437 \u043e\u0445\u043e\u0440\u043e\u043d\u0438", "disarming": "\u0417\u043d\u044f\u0442\u0442\u044f", "pending": "\u041e\u0447\u0456\u043a\u0443\u044e", "triggered": "\u0422\u0440\u0438\u0432\u043e\u0433\u0430" diff --git a/homeassistant/components/alarmdecoder/translations/de.json b/homeassistant/components/alarmdecoder/translations/de.json index 3f1b7ef816e..c37fb7b4390 100644 --- a/homeassistant/components/alarmdecoder/translations/de.json +++ b/homeassistant/components/alarmdecoder/translations/de.json @@ -1,7 +1,10 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "protocol": { @@ -42,7 +45,7 @@ "data": { "zone_number": "Zonennummer" }, - "description": "Geben Sie die Zonennummer ein, die Sie hinzuf\u00fcgen, bearbeiten oder entfernen m\u00f6chten." + "description": "Gib die die Zonennummer ein, die du hinzuf\u00fcgen, bearbeiten oder entfernen m\u00f6chtest." } } } diff --git a/homeassistant/components/alarmdecoder/translations/tr.json b/homeassistant/components/alarmdecoder/translations/tr.json new file mode 100644 index 00000000000..276b733b31f --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/tr.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "protocol": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + } + } + } + }, + "options": { + "error": { + "relay_inclusive": "R\u00f6le Adresi ve R\u00f6le Kanal\u0131 birbirine ba\u011fl\u0131d\u0131r ve birlikte eklenmelidir." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "Alternatif Gece Modu" + } + }, + "init": { + "data": { + "edit_select": "D\u00fczenle" + } + }, + "zone_details": { + "data": { + "zone_name": "B\u00f6lge Ad\u0131", + "zone_relayaddr": "R\u00f6le Adresi", + "zone_relaychan": "R\u00f6le Kanal\u0131" + } + }, + "zone_select": { + "data": { + "zone_number": "B\u00f6lge Numaras\u0131" + }, + "title": "AlarmDecoder'\u0131 yap\u0131land\u0131r\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alarmdecoder/translations/uk.json b/homeassistant/components/alarmdecoder/translations/uk.json new file mode 100644 index 00000000000..c19d00c0eca --- /dev/null +++ b/homeassistant/components/alarmdecoder/translations/uk.json @@ -0,0 +1,74 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "create_entry": { + "default": "\u0423\u0441\u043f\u0456\u0448\u043d\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0434\u043e AlarmDecoder." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "protocol": { + "data": { + "device_baudrate": "\u0428\u0432\u0438\u0434\u043a\u0456\u0441\u0442\u044c \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0456 \u0434\u0430\u043d\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e", + "device_path": "\u0428\u043b\u044f\u0445 \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e", + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f" + }, + "user": { + "data": { + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b" + }, + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b AlarmDecoder" + } + } + }, + "options": { + "error": { + "int": "\u041f\u043e\u043b\u0435 \u043d\u0438\u0436\u0447\u0435 \u043c\u0430\u0454 \u0431\u0443\u0442\u0438 \u0446\u0456\u043b\u0438\u043c \u0447\u0438\u0441\u043b\u043e\u043c.", + "loop_range": "RF Loop \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0446\u0456\u043b\u0438\u043c \u0447\u0438\u0441\u043b\u043e\u043c \u0432\u0456\u0434 1 \u0434\u043e 4.", + "loop_rfid": "RF Loop \u043d\u0435 \u043c\u043e\u0436\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u0431\u0435\u0437 RF Serial.", + "relay_inclusive": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0440\u0435\u043b\u0435 \u0456 \u043a\u0430\u043d\u0430\u043b \u0440\u0435\u043b\u0435 \u0432\u0437\u0430\u0454\u043c\u043e\u0437\u0430\u043b\u0435\u0436\u043d\u0456 \u0456 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0431\u0443\u0442\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0456 \u0440\u0430\u0437\u043e\u043c." + }, + "step": { + "arm_settings": { + "data": { + "alt_night_mode": "\u0410\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u0438\u0439 \u043d\u0456\u0447\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c", + "auto_bypass": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0438\u0439 \u0432\u043a\u043b\u044e\u0447\u0430\u0442\u0438 \u0432\u0438\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u043f\u0440\u0438 \u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u0446\u0456 \u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0443", + "code_arm_required": "\u041a\u043e\u0434, \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u0438\u0439 \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0443" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AlarmDecoder" + }, + "init": { + "data": { + "edit_select": "\u0417\u043c\u0456\u043d\u0438\u0442\u0438" + }, + "description": "\u0429\u043e \u0431 \u0412\u0438 \u0445\u043e\u0442\u0456\u043b\u0438 \u0440\u0435\u0434\u0430\u0433\u0443\u0432\u0430\u0442\u0438?", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AlarmDecoder" + }, + "zone_details": { + "data": { + "zone_loop": "RF Loop", + "zone_name": "\u041d\u0430\u0437\u0432\u0430 \u0437\u043e\u043d\u0438", + "zone_relayaddr": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0440\u0435\u043b\u0435", + "zone_relaychan": "\u041a\u0430\u043d\u0430\u043b \u0440\u0435\u043b\u0435", + "zone_rfid": "RF Serial", + "zone_type": "\u0422\u0438\u043f \u0437\u043e\u043d\u0438" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0434\u0430\u043d\u0456 \u0434\u043b\u044f \u0437\u043e\u043d\u0438 {zone_number}. \u0429\u043e\u0431 \u0432\u0438\u0434\u0430\u043b\u0438\u0442\u0438 \u0437\u043e\u043d\u0443 {zone_number}, \u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u043b\u0435 \"\u041d\u0430\u0437\u0432\u0430 \u0437\u043e\u043d\u0438\" \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AlarmDecoder" + }, + "zone_select": { + "data": { + "zone_number": "\u041d\u043e\u043c\u0435\u0440 \u0437\u043e\u043d\u0438" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043d\u043e\u043c\u0435\u0440 \u0437\u043e\u043d\u0438, \u044f\u043a\u0443 \u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438, \u0437\u043c\u0456\u043d\u0438\u0442\u0438 \u0430\u0431\u043e \u0432\u0438\u0434\u0430\u043b\u0438\u0442\u0438.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f AlarmDecoder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alexa/state_report.py b/homeassistant/components/alexa/state_report.py index 1d06422056d..aa4110ea686 100644 --- a/homeassistant/components/alexa/state_report.py +++ b/homeassistant/components/alexa/state_report.py @@ -2,15 +2,18 @@ import asyncio import json import logging +from typing import Optional import aiohttp import async_timeout from homeassistant.const import HTTP_ACCEPTED, MATCH_ALL, STATE_ON +from homeassistant.core import State +from homeassistant.helpers.significant_change import create_checker import homeassistant.util.dt as dt_util -from .const import API_CHANGE, Cause -from .entities import ENTITY_ADAPTERS, generate_alexa_id +from .const import API_CHANGE, DOMAIN, Cause +from .entities import ENTITY_ADAPTERS, AlexaEntity, generate_alexa_id from .messages import AlexaResponse _LOGGER = logging.getLogger(__name__) @@ -25,7 +28,13 @@ async def async_enable_proactive_mode(hass, smart_home_config): # Validate we can get access token. await smart_home_config.async_get_access_token() - async def async_entity_state_listener(changed_entity, old_state, new_state): + checker = await create_checker(hass, DOMAIN) + + async def async_entity_state_listener( + changed_entity: str, + old_state: Optional[State], + new_state: Optional[State], + ): if not hass.is_running: return @@ -39,24 +48,43 @@ async def async_enable_proactive_mode(hass, smart_home_config): _LOGGER.debug("Not exposing %s because filtered by config", changed_entity) return - alexa_changed_entity = ENTITY_ADAPTERS[new_state.domain]( + alexa_changed_entity: AlexaEntity = ENTITY_ADAPTERS[new_state.domain]( hass, smart_home_config, new_state ) + # Determine how entity should be reported on + should_report = False + should_doorbell = False + for interface in alexa_changed_entity.interfaces(): - if interface.properties_proactively_reported(): - await async_send_changereport_message( - hass, smart_home_config, alexa_changed_entity - ) - return + if not should_report and interface.properties_proactively_reported(): + should_report = True + if ( interface.name() == "Alexa.DoorbellEventSource" and new_state.state == STATE_ON ): - await async_send_doorbell_event_message( - hass, smart_home_config, alexa_changed_entity - ) - return + should_doorbell = True + break + + if not should_report and not should_doorbell: + return + + if not checker.async_is_significant_change(new_state): + return + + if should_doorbell: + should_report = False + + if should_report: + await async_send_changereport_message( + hass, smart_home_config, alexa_changed_entity + ) + + elif should_doorbell: + await async_send_doorbell_event_message( + hass, smart_home_config, alexa_changed_entity + ) return hass.helpers.event.async_track_state_change( MATCH_ALL, async_entity_state_listener @@ -76,13 +104,11 @@ async def async_send_changereport_message( endpoint = alexa_entity.alexa_id() - # this sends all the properties of the Alexa Entity, whether they have - # changed or not. this should be improved, and properties that have not - # changed should be moved to the 'context' object - properties = list(alexa_entity.serialize_properties()) - payload = { - API_CHANGE: {"cause": {"type": Cause.APP_INTERACTION}, "properties": properties} + API_CHANGE: { + "cause": {"type": Cause.APP_INTERACTION}, + "properties": list(alexa_entity.serialize_properties()), + } } message = AlexaResponse(name="ChangeReport", namespace="Alexa", payload=payload) diff --git a/homeassistant/components/almond/translations/de.json b/homeassistant/components/almond/translations/de.json index e3a61026774..5eb8c4940aa 100644 --- a/homeassistant/components/almond/translations/de.json +++ b/homeassistant/components/almond/translations/de.json @@ -1,8 +1,10 @@ { "config": { "abort": { - "cannot_connect": "Verbindung zum Almond-Server nicht m\u00f6glich.", - "missing_configuration": "Bitte \u00fcberpr\u00fcfe die Dokumentation zur Einrichtung von Almond." + "cannot_connect": "Verbindung fehlgeschlagen", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "step": { "hassio_confirm": { diff --git a/homeassistant/components/almond/translations/no.json b/homeassistant/components/almond/translations/no.json index 1b0f03b8018..9cd22ca5bc5 100644 --- a/homeassistant/components/almond/translations/no.json +++ b/homeassistant/components/almond/translations/no.json @@ -8,8 +8,8 @@ }, "step": { "hassio_confirm": { - "description": "Vil du konfigurere Home Assistant til \u00e5 koble til Almond levert av Hass.io tillegget: {addon}?", - "title": "Almond via Hass.io tillegg" + "description": "Vil du konfigurere Home Assistant til \u00e5 koble til Almond levert av Hass.io-tillegg: {addon}?", + "title": "Almond via Hass.io-tillegg" }, "pick_implementation": { "title": "Velg godkjenningsmetode" diff --git a/homeassistant/components/almond/translations/ru.json b/homeassistant/components/almond/translations/ru.json index 27870a46e95..e671651f65d 100644 --- a/homeassistant/components/almond/translations/ru.json +++ b/homeassistant/components/almond/translations/ru.json @@ -8,8 +8,8 @@ }, "step": { "hassio_confirm": { - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a Almond (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io \"{addon}\")?", - "title": "Almond (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io)" + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a Almond (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant \"{addon}\")?", + "title": "Almond (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant)" }, "pick_implementation": { "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" diff --git a/homeassistant/components/almond/translations/tr.json b/homeassistant/components/almond/translations/tr.json new file mode 100644 index 00000000000..dc270099fcd --- /dev/null +++ b/homeassistant/components/almond/translations/tr.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/almond/translations/uk.json b/homeassistant/components/almond/translations/uk.json new file mode 100644 index 00000000000..7f8c12917bb --- /dev/null +++ b/homeassistant/components/almond/translations/uk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.", + "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." + }, + "step": { + "hassio_confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e Almond (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Hass.io \"{addon}\")?", + "title": "Almond (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Hass.io)" + }, + "pick_implementation": { + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/alpha_vantage/manifest.json b/homeassistant/components/alpha_vantage/manifest.json index 28103980869..5ff3122668d 100644 --- a/homeassistant/components/alpha_vantage/manifest.json +++ b/homeassistant/components/alpha_vantage/manifest.json @@ -2,6 +2,6 @@ "domain": "alpha_vantage", "name": "Alpha Vantage", "documentation": "https://www.home-assistant.io/integrations/alpha_vantage", - "requirements": ["alpha_vantage==2.2.0"], + "requirements": ["alpha_vantage==2.3.1"], "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/ambiclimate/translations/de.json b/homeassistant/components/ambiclimate/translations/de.json index e5988f76103..d91fc15f37d 100644 --- a/homeassistant/components/ambiclimate/translations/de.json +++ b/homeassistant/components/ambiclimate/translations/de.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "access_token": "Unbekannter Fehler beim Generieren eines Zugriffstokens." + "access_token": "Unbekannter Fehler beim Generieren eines Zugriffstokens.", + "already_configured": "Konto wurde bereits konfiguriert", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen." }, "create_entry": { "default": "Erfolgreiche Authentifizierung mit Ambiclimate" diff --git a/homeassistant/components/ambiclimate/translations/fr.json b/homeassistant/components/ambiclimate/translations/fr.json index bdbfaea20ef..37ef9549686 100644 --- a/homeassistant/components/ambiclimate/translations/fr.json +++ b/homeassistant/components/ambiclimate/translations/fr.json @@ -2,7 +2,7 @@ "config": { "abort": { "access_token": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'un jeton d'acc\u00e8s.", - "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9", + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation." }, "create_entry": { diff --git a/homeassistant/components/ambiclimate/translations/tr.json b/homeassistant/components/ambiclimate/translations/tr.json new file mode 100644 index 00000000000..bcaeba84558 --- /dev/null +++ b/homeassistant/components/ambiclimate/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/translations/uk.json b/homeassistant/components/ambiclimate/translations/uk.json new file mode 100644 index 00000000000..398665ab667 --- /dev/null +++ b/homeassistant/components/ambiclimate/translations/uk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "access_token": "\u041f\u0440\u0438 \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u0456 \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0443 \u0441\u0442\u0430\u043b\u0430\u0441\u044f \u043f\u043e\u043c\u0438\u043b\u043a\u0430.", + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438." + }, + "create_entry": { + "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." + }, + "error": { + "follow_link": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0437\u0430 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f\u043c \u0456 \u043f\u0440\u043e\u0439\u0434\u0456\u0442\u044c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e, \u043f\u0435\u0440\u0448 \u043d\u0456\u0436 \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0438 \"\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0438\".", + "no_token": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043d\u0435 \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430." + }, + "step": { + "auth": { + "description": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u043f\u043e [\u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f\u043c]({authorization_url}) \u0456 ** \u0414\u043e\u0437\u0432\u043e\u043b\u044c\u0442\u0435 ** \u0434\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u0432\u0430\u0448\u043e\u0433\u043e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 Ambi Climate, \u043f\u043e\u0442\u0456\u043c \u043f\u043e\u0432\u0435\u0440\u043d\u0456\u0442\u044c\u0441\u044f \u0441\u044e\u0434\u0438 \u0456 \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c ** \u041f\u0406\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u0418 **.\n(\u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0432\u043a\u0430\u0437\u0430\u043d\u0438\u0439 URL \u0437\u0432\u043e\u0440\u043e\u0442\u043d\u043e\u0433\u043e \u0432\u0438\u043a\u043b\u0438\u043a\u0443 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u0430\u0454 {cb_url} )", + "title": "Ambi Climate" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/translations/de.json b/homeassistant/components/ambient_station/translations/de.json index 53e6b1f69d6..c6570fee0e3 100644 --- a/homeassistant/components/ambient_station/translations/de.json +++ b/homeassistant/components/ambient_station/translations/de.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Dieser App-Schl\u00fcssel wird bereits verwendet." + "already_configured": "Der Dienst ist bereits konfiguriert" }, "error": { - "invalid_key": "Ung\u00fcltiger API Key und / oder Anwendungsschl\u00fcssel", + "invalid_key": "Ung\u00fcltiger API-Schl\u00fcssel", "no_devices": "Keine Ger\u00e4te im Konto gefunden" }, "step": { diff --git a/homeassistant/components/ambient_station/translations/tr.json b/homeassistant/components/ambient_station/translations/tr.json new file mode 100644 index 00000000000..908d97f5758 --- /dev/null +++ b/homeassistant/components/ambient_station/translations/tr.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_key": "Ge\u00e7ersiz API anahtar\u0131" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ambient_station/translations/uk.json b/homeassistant/components/ambient_station/translations/uk.json new file mode 100644 index 00000000000..722cf99af7e --- /dev/null +++ b/homeassistant/components/ambient_station/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "invalid_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API", + "no_devices": "\u0412 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u043c\u0443 \u0437\u0430\u043f\u0438\u0441\u0456 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "app_key": "\u041a\u043b\u044e\u0447 \u0434\u043e\u0434\u0430\u0442\u043a\u0443" + }, + "title": "Ambient PWS" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/manifest.json b/homeassistant/components/apple_tv/manifest.json index 21b2df308d3..66ae2864dc4 100644 --- a/homeassistant/components/apple_tv/manifest.json +++ b/homeassistant/components/apple_tv/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/apple_tv", "requirements": [ - "pyatv==0.7.5" + "pyatv==0.7.6" ], "zeroconf": [ "_mediaremotetv._tcp.local.", diff --git a/homeassistant/components/apple_tv/translations/fr.json b/homeassistant/components/apple_tv/translations/fr.json index a55d37ed588..e1a719b31c9 100644 --- a/homeassistant/components/apple_tv/translations/fr.json +++ b/homeassistant/components/apple_tv/translations/fr.json @@ -1,7 +1,17 @@ { "config": { + "abort": { + "already_configured_device": "Le p\u00e9riph\u00e9rique est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "La configuration est d\u00e9j\u00e0 en cours", + "backoff": "L'appareil n'accepte pas les demandes d'appariement pour le moment (vous avez peut-\u00eatre saisi un code PIN non valide trop de fois), r\u00e9essayez plus tard.", + "device_did_not_pair": "Aucune tentative pour terminer l'appairage n'a \u00e9t\u00e9 effectu\u00e9e \u00e0 partir de l'appareil.", + "invalid_config": "La configuration de cet appareil est incompl\u00e8te. Veuillez r\u00e9essayer de l'ajouter.", + "no_devices_found": "Aucun appareil trouv\u00e9 sur le r\u00e9seau", + "unknown": "Erreur inattendue" + }, "error": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "invalid_auth": "Autentification invalide", "no_devices_found": "Aucun appareil d\u00e9tect\u00e9 sur le r\u00e9seau", "no_usable_service": "Un dispositif a \u00e9t\u00e9 trouv\u00e9, mais aucun moyen d\u2019\u00e9tablir un lien avec lui. Si vous continuez \u00e0 voir ce message, essayez de sp\u00e9cifier son adresse IP ou de red\u00e9marrer votre Apple TV.", "unknown": "Erreur innatendue" @@ -19,9 +29,12 @@ "pair_with_pin": { "data": { "pin": "Code PIN" - } + }, + "description": "L'appairage est requis pour le protocole `{protocol}`. Veuillez saisir le code PIN affich\u00e9 \u00e0 l'\u00e9cran. Les z\u00e9ros doivent \u00eatre omis, c'est-\u00e0-dire entrer 123 si le code affich\u00e9 est 0123.", + "title": "Appairage" }, "reconfigure": { + "description": "Cette Apple TV rencontre des difficult\u00e9s de connexion et doit \u00eatre reconfigur\u00e9e.", "title": "Reconfiguration de l'appareil" }, "service_problem": { diff --git a/homeassistant/components/apple_tv/translations/lb.json b/homeassistant/components/apple_tv/translations/lb.json index 945f467c4cf..2354033b577 100644 --- a/homeassistant/components/apple_tv/translations/lb.json +++ b/homeassistant/components/apple_tv/translations/lb.json @@ -3,9 +3,14 @@ "abort": { "already_configured_device": "Apparat ass scho konfigur\u00e9iert", "already_in_progress": "Konfiguratioun's Oflaf ass schon am gaang", + "invalid_config": "Konfiguratioun fir d\u00ebsen Apparat ass net komplett. Prob\u00e9ier fir et nach emol dob\u00e4i ze setzen.", + "no_devices_found": "Keng Apparater am Netzwierk fonnt", "unknown": "Onerwaarte Feeler" }, "error": { + "already_configured": "Apparat ass scho konfigur\u00e9iert", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "no_devices_found": "Keng Apparater am Netzwierk fonnt", "unknown": "Onerwaarte Feeler" }, "flow_title": "Apple TV: {name}", @@ -29,6 +34,9 @@ "description": "D\u00ebsen Apple TV huet e puer Verbindungsschwieregkeeten a muss nei konfigur\u00e9iert ginn.", "title": "Apparat Rekonfiguratioun" }, + "service_problem": { + "title": "Feeler beim dob\u00e4isetze vum Service" + }, "user": { "data": { "device_input": "Apparat" diff --git a/homeassistant/components/apple_tv/translations/tr.json b/homeassistant/components/apple_tv/translations/tr.json index 0ddc466a6f7..f33e3998af6 100644 --- a/homeassistant/components/apple_tv/translations/tr.json +++ b/homeassistant/components/apple_tv/translations/tr.json @@ -1,6 +1,8 @@ { "config": { "abort": { + "already_configured_device": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", "invalid_config": "Bu ayg\u0131t\u0131n yap\u0131land\u0131rmas\u0131 tamamlanmad\u0131. L\u00fctfen tekrar eklemeyi deneyin.", "no_devices_found": "A\u011fda cihaz bulunamad\u0131", "unknown": "Beklenmeyen hata" diff --git a/homeassistant/components/apple_tv/translations/uk.json b/homeassistant/components/apple_tv/translations/uk.json new file mode 100644 index 00000000000..a1ae2259ada --- /dev/null +++ b/homeassistant/components/apple_tv/translations/uk.json @@ -0,0 +1,64 @@ +{ + "config": { + "abort": { + "already_configured_device": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "backoff": "\u0412 \u0434\u0430\u043d\u0438\u0439 \u0447\u0430\u0441 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043f\u0440\u0438\u0439\u043c\u0430\u0454 \u0437\u0430\u043f\u0438\u0442\u0438 \u043d\u0430 \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438 (\u043c\u043e\u0436\u043b\u0438\u0432\u043e, \u0412\u0438 \u0437\u0430\u043d\u0430\u0434\u0442\u043e \u0431\u0430\u0433\u0430\u0442\u043e \u0440\u0430\u0437 \u0432\u0432\u043e\u0434\u0438\u043b\u0438 \u043d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 PIN-\u043a\u043e\u0434), \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437 \u043f\u0456\u0437\u043d\u0456\u0448\u0435.", + "device_did_not_pair": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043d\u0430\u043c\u0430\u0433\u0430\u0432\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043f\u0440\u043e\u0446\u0435\u0441 \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438.", + "invalid_config": "\u041a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f \u0446\u044c\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u043d\u0435 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u0430. \u0421\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 \u0439\u043e\u0433\u043e \u0449\u0435 \u0440\u0430\u0437.", + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "no_usable_service": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u0432\u0438\u0437\u043d\u0430\u0447\u0438\u0442\u0438 \u0441\u043f\u043e\u0441\u0456\u0431 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e. \u042f\u043a\u0449\u043e \u0412\u0438 \u0432\u0436\u0435 \u0431\u0430\u0447\u0438\u043b\u0438 \u0446\u0435 \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u043d\u044f, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0432\u043a\u0430\u0437\u0430\u0442\u0438 IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u0430\u0431\u043e \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0456\u0442\u044c \u0439\u043e\u0433\u043e.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "flow_title": "Apple TV: {name}", + "step": { + "confirm": { + "description": "\u0412\u0438 \u0437\u0431\u0438\u0440\u0430\u0454\u0442\u0435\u0441\u044f \u0434\u043e\u0434\u0430\u0442\u0438 Apple TV `{name}` \u0432 Home Assistant. \n\n ** \u0414\u043b\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043d\u044f \u043f\u0440\u043e\u0446\u0435\u0441\u0443 \u0412\u0430\u043c \u043c\u043e\u0436\u0435 \u0437\u043d\u0430\u0434\u043e\u0431\u0438\u0442\u0438\u0441\u044f \u0432\u0432\u0435\u0441\u0442\u0438 \u043a\u0456\u043b\u044c\u043a\u0430 PIN-\u043a\u043e\u0434\u0456\u0432. ** \n\n\u0417\u0432\u0435\u0440\u043d\u0456\u0442\u044c \u0443\u0432\u0430\u0433\u0443, \u0449\u043e \u0412\u0438 *\u043d\u0435* \u0437\u043c\u043e\u0436\u0435\u0442\u0435 \u0432\u0438\u043c\u0438\u043a\u0430\u0442\u0438 Apple TV \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u0446\u0456\u0454\u0457 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457. \u0412 Home Assistant \u043c\u043e\u0436\u043b\u0438\u0432\u043e \u0432\u0438\u043c\u043a\u043d\u0443\u0442\u0438 \u0442\u0456\u043b\u044c\u043a\u0438 \u043c\u0435\u0434\u0456\u0430\u043f\u0440\u043e\u0433\u0440\u0430\u0432\u0430\u0447!", + "title": "\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0456\u0442\u044c \u0434\u043e\u0434\u0430\u0432\u0430\u043d\u043d\u044f Apple TV" + }, + "pair_no_pin": { + "description": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0434\u043b\u044f \u0441\u043b\u0443\u0436\u0431\u0438 `{protocol}`. \u0414\u043b\u044f \u043f\u0440\u043e\u0434\u043e\u0432\u0436\u0435\u043d\u043d\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0432\u0435\u0434\u0456\u0442\u044c PIN-\u043a\u043e\u0434 {pin} \u043d\u0430 \u0412\u0430\u0448\u043e\u043c\u0443 Apple TV.", + "title": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f" + }, + "pair_with_pin": { + "data": { + "pin": "PIN-\u043a\u043e\u0434" + }, + "description": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 `{protocol}`. \u0412\u0432\u0435\u0434\u0456\u0442\u044c PIN-\u043a\u043e\u0434, \u044f\u043a\u0438\u0439 \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u0454\u0442\u044c\u0441\u044f \u043d\u0430 \u0435\u043a\u0440\u0430\u043d\u0456. \u041f\u0435\u0440\u0448\u0456 \u043d\u0443\u043b\u0456 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0431\u0443\u0442\u0438 \u043e\u043f\u0443\u0449\u0435\u043d\u0456, \u0442\u043e\u0431\u0442\u043e \u0432\u0432\u0435\u0434\u0456\u0442\u044c 123, \u044f\u043a\u0449\u043e \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u0454\u0442\u044c\u0441\u044f \u043a\u043e\u0434 0123.", + "title": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f" + }, + "reconfigure": { + "description": "\u0423 \u0446\u044c\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Apple TV \u0432\u0438\u043d\u0438\u043a\u0430\u044e\u0442\u044c \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u043f\u0440\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u0456, \u0439\u043e\u0433\u043e \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u043f\u0435\u0440\u0435\u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438.", + "title": "\u041f\u0435\u0440\u0435\u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + }, + "service_problem": { + "description": "\u0412\u0438\u043d\u0438\u043a\u043b\u0430 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0430 \u043f\u0440\u0438 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u0456 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 `{protocol}`. \u0426\u0435 \u0431\u0443\u0434\u0435 \u043f\u0440\u043e\u0456\u0433\u043d\u043e\u0440\u043e\u0432\u0430\u043d\u043e.", + "title": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0434\u043e\u0434\u0430\u0442\u0438 \u0441\u043b\u0443\u0436\u0431\u0443" + }, + "user": { + "data": { + "device_input": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + }, + "description": "\u041f\u043e\u0447\u043d\u0456\u0442\u044c \u0437 \u0432\u0432\u0435\u0434\u0435\u043d\u043d\u044f \u043d\u0430\u0437\u0432\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434, \u041a\u0443\u0445\u043d\u044f \u0430\u0431\u043e \u0421\u043f\u0430\u043b\u044c\u043d\u044f) \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0438 Apple TV, \u044f\u043a\u0443 \u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438. \u042f\u043a\u0449\u043e \u0431\u0443\u0434\u044c-\u044f\u043a\u0456 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u0431\u0443\u043b\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u0456 \u0443 \u0412\u0430\u0448\u0456\u0439 \u043c\u0435\u0440\u0435\u0436\u0456, \u0432\u043e\u043d\u0438 \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u0456 \u043d\u0438\u0436\u0447\u0435. \n\n \u042f\u043a\u0449\u043e \u0412\u0438 \u043d\u0435 \u0431\u0430\u0447\u0438\u0442\u0435 \u0441\u0432\u0456\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0430\u0431\u043e \u0432\u0438\u043d\u0438\u043a\u0430\u044e\u0442\u044c \u0431\u0443\u0434\u044c-\u044f\u043a\u0456 \u0456\u043d\u0448\u0456 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u043f\u0456\u0434 \u0447\u0430\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0432\u043a\u0430\u0437\u0430\u0442\u0438 IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e. \n\n {devices}", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043d\u043e\u0432\u043e\u0433\u043e Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "\u041d\u0435 \u0432\u043c\u0438\u043a\u0430\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0440\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0443 Home Assistant" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + }, + "title": "Apple TV" +} \ No newline at end of file diff --git a/homeassistant/components/apple_tv/translations/zh-Hans.json b/homeassistant/components/apple_tv/translations/zh-Hans.json index bb1f8e025ca..54095a0a633 100644 --- a/homeassistant/components/apple_tv/translations/zh-Hans.json +++ b/homeassistant/components/apple_tv/translations/zh-Hans.json @@ -1,6 +1,9 @@ { "config": { "step": { + "confirm": { + "description": "\u60a8\u5373\u5c06\u6dfb\u52a0 Apple TV (\u540d\u79f0\u4e3a\u201c{name}\u201d)\u5230 Home Assistant\u3002 \n\n **\u8981\u5b8c\u6210\u6b64\u8fc7\u7a0b\uff0c\u53ef\u80fd\u9700\u8981\u8f93\u5165\u591a\u4e2a PIN \u7801\u3002** \n\n\u8bf7\u6ce8\u610f\uff0c\u6b64\u96c6\u6210*\u4e0d\u80fd*\u5173\u95ed Apple TV \u7684\u7535\u6e90\uff0c\u53ea\u4f1a\u5173\u95ed Home Assistant \u4e2d\u7684\u5a92\u4f53\u64ad\u653e\u5668\uff01" + }, "pair_no_pin": { "title": "\u914d\u5bf9\u4e2d" }, @@ -8,6 +11,20 @@ "data": { "pin": "PIN\u7801" } + }, + "user": { + "description": "\u8981\u5f00\u59cb\uff0c\u8bf7\u8f93\u5165\u8981\u6dfb\u52a0\u7684 Apple TV \u7684\u8bbe\u5907\u540d\u79f0\u6216 IP \u5730\u5740\u3002\u5728\u7f51\u7edc\u4e0a\u81ea\u52a8\u53d1\u73b0\u7684\u8bbe\u5907\u4f1a\u663e\u793a\u5728\u4e0b\u65b9\u3002 \n\n\u5982\u679c\u6ca1\u6709\u53d1\u73b0\u8bbe\u5907\u6216\u9047\u5230\u4efb\u4f55\u95ee\u9898\uff0c\u8bf7\u5c1d\u8bd5\u6307\u5b9a\u8bbe\u5907 IP \u5730\u5740\u3002 \n\n {devices}", + "title": "\u8bbe\u7f6e\u65b0\u7684 Apple TV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "start_off": "\u542f\u52a8 Home Assistant \u65f6\u4e0d\u6253\u5f00\u8bbe\u5907" + }, + "description": "\u914d\u7f6e\u8bbe\u5907\u901a\u7528\u8bbe\u7f6e" } } }, diff --git a/homeassistant/components/arcam_fmj/__init__.py b/homeassistant/components/arcam_fmj/__init__.py index 0175dfd6586..686e7c2de16 100644 --- a/homeassistant/components/arcam_fmj/__init__.py +++ b/homeassistant/components/arcam_fmj/__init__.py @@ -108,8 +108,6 @@ async def _run_client(hass, client, interval): await asyncio.sleep(interval) except asyncio.TimeoutError: continue - except asyncio.CancelledError: - raise except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception, aborting arcam client") return diff --git a/homeassistant/components/arcam_fmj/translations/de.json b/homeassistant/components/arcam_fmj/translations/de.json index 92ad0e22663..b7270e730bb 100644 --- a/homeassistant/components/arcam_fmj/translations/de.json +++ b/homeassistant/components/arcam_fmj/translations/de.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "cannot_connect": "Verbindungsfehler" + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "user": { diff --git a/homeassistant/components/arcam_fmj/translations/ro.json b/homeassistant/components/arcam_fmj/translations/ro.json new file mode 100644 index 00000000000..a8008f1e8bc --- /dev/null +++ b/homeassistant/components/arcam_fmj/translations/ro.json @@ -0,0 +1,9 @@ +{ + "config": { + "error": { + "few": "Pu\u021bine", + "one": "Unul", + "other": "Altele" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/tr.json b/homeassistant/components/arcam_fmj/translations/tr.json new file mode 100644 index 00000000000..dd15f57212c --- /dev/null +++ b/homeassistant/components/arcam_fmj/translations/tr.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/arcam_fmj/translations/uk.json b/homeassistant/components/arcam_fmj/translations/uk.json new file mode 100644 index 00000000000..4d33a5bc0d9 --- /dev/null +++ b/homeassistant/components/arcam_fmj/translations/uk.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "flow_title": "Arcam FMJ {host}", + "step": { + "confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 Arcam FMJ `{host}`?" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_on": "\u0437\u0430\u043f\u0438\u0442\u0430\u043d\u043e \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043d\u044f {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/atag/translations/de.json b/homeassistant/components/atag/translations/de.json index 2ced7577fdf..b94103d898b 100644 --- a/homeassistant/components/atag/translations/de.json +++ b/homeassistant/components/atag/translations/de.json @@ -1,15 +1,15 @@ { "config": { "abort": { - "already_configured": "Dieses Ger\u00e4t wurde bereits zu HomeAssistant hinzugef\u00fcgt" + "already_configured": "Dieses Ger\u00e4t wurde bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "user": { "data": { - "email": "Email (Optional)", + "email": "E-Mail", "host": "Host", "port": "Port" }, diff --git a/homeassistant/components/atag/translations/tr.json b/homeassistant/components/atag/translations/tr.json new file mode 100644 index 00000000000..f7c94d0a976 --- /dev/null +++ b/homeassistant/components/atag/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unauthorized": "E\u015fle\u015ftirme reddedildi, kimlik do\u011frulama iste\u011fi i\u00e7in cihaz\u0131 kontrol edin" + }, + "step": { + "user": { + "data": { + "email": "E-posta", + "host": "Ana Bilgisayar", + "port": "Port" + }, + "title": "Cihaza ba\u011flan\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/atag/translations/uk.json b/homeassistant/components/atag/translations/uk.json new file mode 100644 index 00000000000..ee0a077d900 --- /dev/null +++ b/homeassistant/components/atag/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unauthorized": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f \u0437\u0430\u0431\u043e\u0440\u043e\u043d\u0435\u043d\u043e, \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0430 \u0437\u0430\u043f\u0438\u0442 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457." + }, + "step": { + "user": { + "data": { + "email": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438", + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index feaf61450e8..6f16f7d5b31 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -43,17 +43,22 @@ _LOGGER = logging.getLogger(__name__) TWO_FA_REVALIDATE = "verify_configurator" CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_LOGIN_METHOD): vol.In(LOGIN_METHODS), - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_INSTALL_ID): cv.string, - vol.Optional(CONF_TIMEOUT, default=DEFAULT_TIMEOUT): cv.positive_int, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_LOGIN_METHOD): vol.In(LOGIN_METHODS), + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_INSTALL_ID): cv.string, + vol.Optional( + CONF_TIMEOUT, default=DEFAULT_TIMEOUT + ): cv.positive_int, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) diff --git a/homeassistant/components/august/activity.py b/homeassistant/components/august/activity.py index c7a7d68d959..d972fbf5281 100644 --- a/homeassistant/components/august/activity.py +++ b/homeassistant/components/august/activity.py @@ -11,7 +11,7 @@ from .subscriber import AugustSubscriberMixin _LOGGER = logging.getLogger(__name__) ACTIVITY_STREAM_FETCH_LIMIT = 10 -ACTIVITY_CATCH_UP_FETCH_LIMIT = 1000 +ACTIVITY_CATCH_UP_FETCH_LIMIT = 2500 class ActivityStream(AugustSubscriberMixin): @@ -102,11 +102,14 @@ class ActivityStream(AugustSubscriberMixin): def _process_newer_device_activities(self, activities): updated_device_ids = set() for activity in activities: - self._latest_activities_by_id_type.setdefault(activity.device_id, {}) + device_id = activity.device_id + activity_type = activity.activity_type - lastest_activity = self._latest_activities_by_id_type[ - activity.device_id - ].get(activity.activity_type) + self._latest_activities_by_id_type.setdefault(device_id, {}) + + lastest_activity = self._latest_activities_by_id_type[device_id].get( + activity_type + ) # Ignore activities that are older than the latest one if ( @@ -115,10 +118,8 @@ class ActivityStream(AugustSubscriberMixin): ): continue - self._latest_activities_by_id_type[activity.device_id][ - activity.activity_type - ] = activity + self._latest_activities_by_id_type[device_id][activity_type] = activity - updated_device_ids.add(activity.device_id) + updated_device_ids.add(device_id) return updated_device_ids diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index 67649b7edba..dcdfb0a0497 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -5,5 +5,9 @@ "requirements": ["py-august==0.25.2"], "dependencies": ["configurator"], "codeowners": ["@bdraco"], + "dhcp": [ + {"hostname":"connect","macaddress":"D86162*"}, + {"hostname":"connect","macaddress":"B8B7F1*"} + ], "config_flow": true } diff --git a/homeassistant/components/august/sensor.py b/homeassistant/components/august/sensor.py index c3f5f05ceef..6004a07f605 100644 --- a/homeassistant/components/august/sensor.py +++ b/homeassistant/components/august/sensor.py @@ -4,7 +4,7 @@ import logging from august.activity import ActivityType from homeassistant.components.sensor import DEVICE_CLASS_BATTERY -from homeassistant.const import ATTR_ENTITY_PICTURE, PERCENTAGE +from homeassistant.const import ATTR_ENTITY_PICTURE, PERCENTAGE, STATE_UNAVAILABLE from homeassistant.core import callback from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_registry import async_get_registry @@ -157,8 +157,8 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, Entity): self._device_id, [ActivityType.LOCK_OPERATION] ) + self._available = True if lock_activity is not None: - self._available = True self._state = lock_activity.operated_by self._operated_remote = lock_activity.operated_remote self._operated_keypad = lock_activity.operated_keypad @@ -193,7 +193,7 @@ class AugustOperatorSensor(AugustEntityMixin, RestoreEntity, Entity): await super().async_added_to_hass() last_state = await self.async_get_last_state() - if not last_state: + if not last_state or last_state.state == STATE_UNAVAILABLE: return self._state = last_state.state diff --git a/homeassistant/components/august/translations/de.json b/homeassistant/components/august/translations/de.json index d46be650e2c..3a5bd70f1af 100644 --- a/homeassistant/components/august/translations/de.json +++ b/homeassistant/components/august/translations/de.json @@ -1,10 +1,11 @@ { "config": { "abort": { - "already_configured": "Konto ist bereits konfiguriert" + "already_configured": "Konto wurde bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/august/translations/tr.json b/homeassistant/components/august/translations/tr.json new file mode 100644 index 00000000000..ccb9e200c82 --- /dev/null +++ b/homeassistant/components/august/translations/tr.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "login_method": "Giri\u015f Y\u00f6ntemi", + "password": "Parola", + "timeout": "Zaman a\u015f\u0131m\u0131 (saniye)", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "Giri\u015f Y\u00f6ntemi 'e-posta' ise, Kullan\u0131c\u0131 Ad\u0131 e-posta adresidir. Giri\u015f Y\u00f6ntemi 'telefon' ise, Kullan\u0131c\u0131 Ad\u0131 '+ NNNNNNNNN' bi\u00e7imindeki telefon numaras\u0131d\u0131r." + }, + "validation": { + "data": { + "code": "Do\u011frulama kodu" + }, + "description": "L\u00fctfen {login_method} ( {username} ) bilgilerinizi kontrol edin ve a\u015fa\u011f\u0131ya do\u011frulama kodunu girin", + "title": "\u0130ki fakt\u00f6rl\u00fc kimlik do\u011frulama" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/august/translations/uk.json b/homeassistant/components/august/translations/uk.json new file mode 100644 index 00000000000..e06c5347d73 --- /dev/null +++ b/homeassistant/components/august/translations/uk.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "login_method": "\u0421\u043f\u043e\u0441\u0456\u0431 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u042f\u043a\u0449\u043e \u0441\u043f\u043e\u0441\u043e\u0431\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457 \u0432\u0438\u0431\u0440\u0430\u043d\u043e 'email', \u0442\u043e \u043b\u043e\u0433\u0456\u043d\u043e\u043c \u0454 \u0430\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438. \u042f\u043a\u0449\u043e \u0441\u043f\u043e\u0441\u043e\u0431\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457 \u0432\u0438\u0431\u0440\u0430\u043d\u043e 'phone', \u0442\u043e \u043b\u043e\u0433\u0456\u043d\u043e\u043c \u0454 \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0443 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 '+ NNNNNNNNN'.", + "title": "August" + }, + "validation": { + "data": { + "code": "\u041a\u043e\u0434 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f" + }, + "description": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 {login_method} ({username}) \u0456 \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u0438\u0439 \u043a\u043e\u0434 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f.", + "title": "\u0414\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora/translations/de.json b/homeassistant/components/aurora/translations/de.json index 95312fe7943..838673e8d60 100644 --- a/homeassistant/components/aurora/translations/de.json +++ b/homeassistant/components/aurora/translations/de.json @@ -1,7 +1,16 @@ { "config": { "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "user": { + "data": { + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad", + "name": "Name" + } + } } }, "options": { @@ -12,5 +21,6 @@ } } } - } + }, + "title": "NOAA Aurora-Sensor" } \ No newline at end of file diff --git a/homeassistant/components/aurora/translations/fr.json b/homeassistant/components/aurora/translations/fr.json new file mode 100644 index 00000000000..473ecefdbd9 --- /dev/null +++ b/homeassistant/components/aurora/translations/fr.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00c9chec \u00e0 la connexion" + }, + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude", + "name": "Nom" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "Seuil (%)" + } + } + } + }, + "title": "Capteur NOAA Aurora" +} \ No newline at end of file diff --git a/homeassistant/components/aurora/translations/tr.json b/homeassistant/components/aurora/translations/tr.json new file mode 100644 index 00000000000..0c3bb75ed6e --- /dev/null +++ b/homeassistant/components/aurora/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "latitude": "Enlem", + "longitude": "Boylam", + "name": "Ad" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aurora/translations/uk.json b/homeassistant/components/aurora/translations/uk.json new file mode 100644 index 00000000000..0cb3c4fcbce --- /dev/null +++ b/homeassistant/components/aurora/translations/uk.json @@ -0,0 +1,26 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "threshold": "\u041f\u043e\u0440\u0456\u0433 (%)" + } + } + } + }, + "title": "NOAA Aurora Sensor" +} \ No newline at end of file diff --git a/homeassistant/components/auth/translations/de.json b/homeassistant/components/auth/translations/de.json index 06da3cde1a1..93cbf1073cc 100644 --- a/homeassistant/components/auth/translations/de.json +++ b/homeassistant/components/auth/translations/de.json @@ -25,7 +25,7 @@ }, "step": { "init": { - "description": "Um die Zwei-Faktor-Authentifizierung mit zeitbasierten Einmalpassw\u00f6rtern zu aktivieren, scanne den QR-Code mit Ihrer Authentifizierungs-App. Wenn du keine hast, empfehlen wir entweder [Google Authenticator] (https://support.google.com/accounts/answer/1066447) oder [Authy] (https://authy.com/). \n\n {qr_code} \n \nNachdem du den Code gescannt hast, gebe den sechsstelligen Code aus der App ein, um das Setup zu \u00fcberpr\u00fcfen. Wenn es Probleme beim Scannen des QR-Codes gibt, f\u00fchre ein manuelles Setup mit dem Code ** ` {code} ` ** durch.", + "description": "Um die Zwei-Faktor-Authentifizierung mit zeitbasierten Einmalpassw\u00f6rtern zu aktivieren, scanne den QR-Code mit deiner Authentifizierungs-App. Wenn du keine hast, empfehlen wir entweder [Google Authenticator] (https://support.google.com/accounts/answer/1066447) oder [Authy] (https://authy.com/). \n\n {qr_code} \n \nNachdem du den Code gescannt hast, gibst du den sechsstelligen Code aus der App ein, um das Setup zu \u00fcberpr\u00fcfen. Wenn es Probleme beim Scannen des QR-Codes gibt, f\u00fchre ein manuelles Setup mit dem Code ** ` {code} ` ** durch.", "title": "Richte die Zwei-Faktor-Authentifizierung mit TOTP ein" } }, diff --git a/homeassistant/components/auth/translations/tr.json b/homeassistant/components/auth/translations/tr.json new file mode 100644 index 00000000000..7d273214574 --- /dev/null +++ b/homeassistant/components/auth/translations/tr.json @@ -0,0 +1,22 @@ +{ + "mfa_setup": { + "notify": { + "step": { + "init": { + "title": "Bilgilendirme bile\u015feni taraf\u0131ndan verilen tek seferlik parolay\u0131 ayarlay\u0131n" + }, + "setup": { + "description": "**bildirim yoluyla tek seferlik bir parola g\u00f6nderildi. {notify_service}**. L\u00fctfen a\u015fa\u011f\u0131da girin:" + } + }, + "title": "Tek Seferlik Parolay\u0131 Bildir" + }, + "totp": { + "step": { + "init": { + "description": "Zamana dayal\u0131 tek seferlik parolalar\u0131 kullanarak iki fakt\u00f6rl\u00fc kimlik do\u011frulamay\u0131 etkinle\u015ftirmek i\u00e7in kimlik do\u011frulama uygulaman\u0131zla QR kodunu taray\u0131n. Hesab\u0131n\u0131z yoksa, [Google Authenticator] (https://support.google.com/accounts/answer/1066447) veya [Authy] (https://authy.com/) \u00f6neririz. \n\n {qr_code}\n\n Kodu tarad\u0131ktan sonra, kurulumu do\u011frulamak i\u00e7in uygulaman\u0131zdan alt\u0131 haneli kodu girin. QR kodunu taramayla ilgili sorun ya\u015f\u0131yorsan\u0131z, ** ` {code} ` manuel kurulum yap\u0131n." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/auth/translations/uk.json b/homeassistant/components/auth/translations/uk.json index f826075078e..eeb8f1ee7c7 100644 --- a/homeassistant/components/auth/translations/uk.json +++ b/homeassistant/components/auth/translations/uk.json @@ -1,14 +1,35 @@ { "mfa_setup": { "notify": { + "abort": { + "no_available_service": "\u041d\u0435\u043c\u0430\u0454 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0445 \u0441\u043b\u0443\u0436\u0431 \u0441\u043f\u043e\u0432\u0456\u0449\u0435\u043d\u044c." + }, "error": { "invalid_code": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043a\u043e\u0434, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437." }, "step": { + "init": { + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u0434\u043d\u0443 \u0456\u0437 \u0441\u043b\u0443\u0436\u0431 \u0441\u043f\u043e\u0432\u0456\u0449\u0435\u043d\u044c:", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043e\u0441\u0442\u0430\u0432\u043a\u0438 \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u0438\u0445 \u043f\u0430\u0440\u043e\u043b\u0456\u0432 \u0447\u0435\u0440\u0435\u0437 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0441\u043f\u043e\u0432\u0456\u0449\u0435\u043d\u044c" + }, "setup": { + "description": "\u041e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u0438\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0439 \u0447\u0435\u0440\u0435\u0437 ** notify.{notify_service} **. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u0439\u043e\u0433\u043e \u043d\u0438\u0436\u0447\u0435:", "title": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f" } - } + }, + "title": "\u0414\u043e\u0441\u0442\u0430\u0432\u043a\u0430 \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u0438\u0445 \u043f\u0430\u0440\u043e\u043b\u0456\u0432" + }, + "totp": { + "error": { + "invalid_code": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043a\u043e\u0434. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443. \u042f\u043a\u0449\u043e \u0412\u0438 \u043f\u043e\u0441\u0442\u0456\u0439\u043d\u043e \u043e\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u0435 \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443, \u0431\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0433\u043e\u0434\u0438\u043d\u043d\u0438\u043a \u0443 \u0412\u0430\u0448\u0456\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0456 Home Assistant \u043f\u043e\u043a\u0430\u0437\u0443\u0454 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439 \u0447\u0430\u0441." + }, + "step": { + "init": { + "description": "\u0429\u043e\u0431 \u0430\u043a\u0442\u0438\u0432\u0443\u0432\u0430\u0442\u0438 \u0434\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0443 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0437 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f\u043c \u043e\u0434\u043d\u043e\u0440\u0430\u0437\u043e\u0432\u0438\u0445 \u043f\u0430\u0440\u043e\u043b\u0456\u0432, \u0437\u0430\u0441\u043d\u043e\u0432\u0430\u043d\u0438\u0445 \u043d\u0430 \u0447\u0430\u0441\u0456, \u0432\u0456\u0434\u0441\u043a\u0430\u043d\u0443\u0439\u0442\u0435 QR-\u043a\u043e\u0434 \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0438 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0438 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0447\u043d\u043e\u0441\u0442\u0456. \u042f\u043a\u0449\u043e \u0443 \u0412\u0430\u0441 \u0457\u0457 \u043d\u0435\u043c\u0430\u0454, \u043c\u0438 \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0454\u043c\u043e \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0430\u0431\u043e [Google Authenticator](https://support.google.com/accounts/answer/1066447), \u0430\u0431\u043e [Authy](https://authy.com/). \n\n{qr_code}\n\n\u041f\u0456\u0441\u043b\u044f \u0441\u043a\u0430\u043d\u0443\u0432\u0430\u043d\u043d\u044f QR-\u043a\u043e\u0434\u0443 \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u0448\u0435\u0441\u0442\u0438\u0437\u043d\u0430\u0447\u043d\u0438\u0439 \u043a\u043e\u0434 \u0437 \u0412\u0430\u0448\u043e\u0433\u043e \u0437\u0430\u0441\u0442\u043e\u0441\u0443\u0432\u0430\u043d\u043d\u044f, \u0449\u043e\u0431 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u042f\u043a\u0449\u043e \u0443 \u0412\u0430\u0441 \u0454 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0437\u0456 \u0441\u043a\u0430\u043d\u0443\u0432\u0430\u043d\u043d\u044f\u043c QR-\u043a\u043e\u0434\u0443, \u0432\u0438\u043a\u043e\u043d\u0430\u0439\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u043a\u043e\u0434\u0443 ** `{code}` **.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0457 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457 \u0437 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f\u043c TOTP" + } + }, + "title": "TOTP" } } } \ No newline at end of file diff --git a/homeassistant/components/automation/__init__.py b/homeassistant/components/automation/__init__.py index e693d2ed814..201eeb5c456 100644 --- a/homeassistant/components/automation/__init__.py +++ b/homeassistant/components/automation/__init__.py @@ -404,6 +404,12 @@ class AutomationEntity(ToggleEntity, RestoreEntity): await self.action_script.async_run( variables, trigger_context, started_action ) + except (vol.Invalid, HomeAssistantError) as err: + self._logger.error( + "Error while executing automation %s: %s", + self.entity_id, + err, + ) except Exception: # pylint: disable=broad-except self._logger.exception("While executing automation %s", self.entity_id) @@ -462,8 +468,8 @@ class AutomationEntity(ToggleEntity, RestoreEntity): ) -> Optional[Callable[[], None]]: """Set up the triggers.""" - def log_cb(level, msg): - self._logger.log(level, "%s %s", msg, self._name) + def log_cb(level, msg, **kwargs): + self._logger.log(level, "%s %s", msg, self._name, **kwargs) return await async_initialize_triggers( cast(HomeAssistant, self.hass), diff --git a/homeassistant/components/awair/translations/de.json b/homeassistant/components/awair/translations/de.json index fcdcd0190e3..5b65ece083b 100644 --- a/homeassistant/components/awair/translations/de.json +++ b/homeassistant/components/awair/translations/de.json @@ -1,17 +1,25 @@ { "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, "error": { - "unknown": "Unbekannter Awair-API-Fehler." + "invalid_access_token": "Ung\u00fcltiger Zugriffs-Token", + "unknown": "Unerwarteter Fehler" }, "step": { "reauth": { "data": { + "access_token": "Zugangstoken", "email": "E-Mail" }, - "description": "Bitte geben Sie Ihr Awair-Entwicklerzugriffstoken erneut ein." + "description": "Bitte gib dein Awair-Entwicklerzugriffstoken erneut ein." }, "user": { "data": { + "access_token": "Zugangstoken", "email": "E-Mail" } } diff --git a/homeassistant/components/awair/translations/tr.json b/homeassistant/components/awair/translations/tr.json new file mode 100644 index 00000000000..84da92b97d3 --- /dev/null +++ b/homeassistant/components/awair/translations/tr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "invalid_access_token": "Ge\u00e7ersiz eri\u015fim belirteci", + "unknown": "Beklenmeyen hata" + }, + "step": { + "reauth": { + "data": { + "access_token": "Eri\u015fim Belirteci", + "email": "E-posta" + } + }, + "user": { + "data": { + "access_token": "Eri\u015fim Belirteci", + "email": "E-posta" + }, + "description": "Awair geli\u015ftirici eri\u015fim belirteci i\u00e7in \u015fu adresten kaydolmal\u0131s\u0131n\u0131z: https://developer.getawair.com/onboard/login" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/awair/translations/uk.json b/homeassistant/components/awair/translations/uk.json new file mode 100644 index 00000000000..f8150ad7faf --- /dev/null +++ b/homeassistant/components/awair/translations/uk.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" + }, + "error": { + "invalid_access_token": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "reauth": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443", + "email": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438" + }, + "description": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0412\u0430\u0448 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443." + }, + "user": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443", + "email": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438" + }, + "description": "\u0414\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0443 \u0434\u043e Awair \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u0437\u0430 \u0430\u0434\u0440\u0435\u0441\u043e\u044e: https://developer.getawair.com/onboard/login" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/aws/manifest.json b/homeassistant/components/aws/manifest.json index fb29418b376..bd9c76cc397 100644 --- a/homeassistant/components/aws/manifest.json +++ b/homeassistant/components/aws/manifest.json @@ -3,5 +3,5 @@ "name": "Amazon Web Services (AWS)", "documentation": "https://www.home-assistant.io/integrations/aws", "requirements": ["aiobotocore==0.11.1"], - "codeowners": ["@awarecan"] + "codeowners": [] } diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index f25348b44cd..c467359c17e 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -2,7 +2,10 @@ import logging -from homeassistant.const import CONF_DEVICE, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import CONF_DEVICE, CONF_MAC, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import callback +from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.entity_registry import async_migrate_entries from .const import DOMAIN as AXIS_DOMAIN from .device import AxisNetworkDevice @@ -24,12 +27,6 @@ async def async_setup_entry(hass, config_entry): if not await device.async_setup(): return False - # 0.104 introduced config entry unique id, this makes upgrading possible - if config_entry.unique_id is None: - hass.config_entries.async_update_entry( - config_entry, unique_id=device.api.vapix.serial_number - ) - hass.data[AXIS_DOMAIN][config_entry.unique_id] = device await device.async_update_device_registry() @@ -52,9 +49,28 @@ async def async_migrate_entry(hass, config_entry): # Flatten configuration but keep old data if user rollbacks HASS prior to 0.106 if config_entry.version == 1: config_entry.data = {**config_entry.data, **config_entry.data[CONF_DEVICE]} - + config_entry.unique_id = config_entry.data[CONF_MAC] config_entry.version = 2 + # Normalise MAC address of device which also affects entity unique IDs + if config_entry.version == 2: + old_unique_id = config_entry.unique_id + new_unique_id = format_mac(old_unique_id) + + @callback + def update_unique_id(entity_entry): + """Update unique ID of entity entry.""" + return { + "new_unique_id": entity_entry.unique_id.replace( + old_unique_id, new_unique_id + ) + } + + await async_migrate_entries(hass, config_entry.entry_id, update_unique_id) + + config_entry.unique_id = new_unique_id + config_entry.version = 3 + _LOGGER.info("Migration to version %s successful", config_entry.version) return True diff --git a/homeassistant/components/axis/axis_base.py b/homeassistant/components/axis/axis_base.py index 976e779c20e..3e2b1a48eb7 100644 --- a/homeassistant/components/axis/axis_base.py +++ b/homeassistant/components/axis/axis_base.py @@ -30,7 +30,7 @@ class AxisEntityBase(Entity): @property def device_info(self): """Return a device description for device registry.""" - return {"identifiers": {(AXIS_DOMAIN, self.device.serial)}} + return {"identifiers": {(AXIS_DOMAIN, self.device.unique_id)}} @callback def update_callback(self, no_delay=None): @@ -73,4 +73,4 @@ class AxisEventBase(AxisEntityBase): @property def unique_id(self): """Return a unique identifier for this device.""" - return f"{self.device.serial}-{self.event.topic}-{self.event.id}" + return f"{self.device.unique_id}-{self.event.topic}-{self.event.id}" diff --git a/homeassistant/components/axis/binary_sensor.py b/homeassistant/components/axis/binary_sensor.py index 1881fe887f9..32d4afa328d 100644 --- a/homeassistant/components/axis/binary_sensor.py +++ b/homeassistant/components/axis/binary_sensor.py @@ -7,10 +7,12 @@ from axis.event_stream import ( CLASS_LIGHT, CLASS_MOTION, CLASS_OUTPUT, + CLASS_PTZ, CLASS_SOUND, FenceGuard, LoiteringGuard, MotionGuard, + ObjectAnalytics, Vmd4, ) @@ -46,7 +48,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Add binary sensor from Axis device.""" event = device.api.event[event_id] - if event.CLASS != CLASS_OUTPUT and not ( + if event.CLASS not in (CLASS_OUTPUT, CLASS_PTZ) and not ( event.CLASS == CLASS_LIGHT and event.TYPE == "Light" ): async_add_entities([AxisBinarySensor(event, device)]) @@ -101,7 +103,7 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity): """Return the name of the event.""" if ( self.event.CLASS == CLASS_INPUT - and self.event.id + and self.event.id in self.device.api.vapix.ports and self.device.api.vapix.ports[self.event.id].name ): return ( @@ -114,6 +116,7 @@ class AxisBinarySensor(AxisEventBase, BinarySensorEntity): (FenceGuard, self.device.api.vapix.fence_guard), (LoiteringGuard, self.device.api.vapix.loitering_guard), (MotionGuard, self.device.api.vapix.motion_guard), + (ObjectAnalytics, self.device.api.vapix.object_analytics), (Vmd4, self.device.api.vapix.vmd4), ): if ( diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py index 69047268b07..cf2634b8f3a 100644 --- a/homeassistant/components/axis/camera.py +++ b/homeassistant/components/axis/camera.py @@ -1,5 +1,7 @@ """Support for Axis camera streaming.""" +from urllib.parse import urlencode + from homeassistant.components.camera import SUPPORT_STREAM from homeassistant.components.mjpeg.camera import ( CONF_MJPEG_URL, @@ -11,14 +13,13 @@ from homeassistant.const import ( CONF_AUTHENTICATION, CONF_NAME, CONF_PASSWORD, - CONF_PORT, CONF_USERNAME, HTTP_DIGEST_AUTHENTICATION, ) from homeassistant.helpers.dispatcher import async_dispatcher_connect from .axis_base import AxisEntityBase -from .const import DEFAULT_STREAM_PROFILE, DOMAIN as AXIS_DOMAIN +from .const import DEFAULT_STREAM_PROFILE, DEFAULT_VIDEO_SOURCE, DOMAIN as AXIS_DOMAIN async def async_setup_entry(hass, config_entry, async_add_entities): @@ -41,9 +42,9 @@ class AxisCamera(AxisEntityBase, MjpegCamera): AxisEntityBase.__init__(self, device) config = { - CONF_NAME: device.config_entry.data[CONF_NAME], - CONF_USERNAME: device.config_entry.data[CONF_USERNAME], - CONF_PASSWORD: device.config_entry.data[CONF_PASSWORD], + CONF_NAME: device.name, + CONF_USERNAME: device.username, + CONF_PASSWORD: device.password, CONF_MJPEG_URL: self.mjpeg_source, CONF_STILL_IMAGE_URL: self.image_source, CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION, @@ -61,38 +62,55 @@ class AxisCamera(AxisEntityBase, MjpegCamera): await super().async_added_to_hass() @property - def supported_features(self): + def supported_features(self) -> int: """Return supported features.""" return SUPPORT_STREAM - def _new_address(self): + def _new_address(self) -> None: """Set new device address for video stream.""" self._mjpeg_url = self.mjpeg_source self._still_image_url = self.image_source @property - def unique_id(self): + def unique_id(self) -> str: """Return a unique identifier for this device.""" - return f"{self.device.serial}-camera" + return f"{self.device.unique_id}-camera" @property - def image_source(self): + def image_source(self) -> str: """Return still image URL for device.""" - return f"http://{self.device.host}:{self.device.config_entry.data[CONF_PORT]}/axis-cgi/jpg/image.cgi" + options = self.generate_options(skip_stream_profile=True) + return f"http://{self.device.host}:{self.device.port}/axis-cgi/jpg/image.cgi{options}" @property - def mjpeg_source(self): + def mjpeg_source(self) -> str: """Return mjpeg URL for device.""" - options = "" - if self.device.option_stream_profile != DEFAULT_STREAM_PROFILE: - options = f"?&streamprofile={self.device.option_stream_profile}" + options = self.generate_options() + return f"http://{self.device.host}:{self.device.port}/axis-cgi/mjpg/video.cgi{options}" - return f"http://{self.device.host}:{self.device.config_entry.data[CONF_PORT]}/axis-cgi/mjpg/video.cgi{options}" - - async def stream_source(self): + async def stream_source(self) -> str: """Return the stream source.""" - options = "" - if self.device.option_stream_profile != DEFAULT_STREAM_PROFILE: - options = f"&streamprofile={self.device.option_stream_profile}" + options = self.generate_options(add_video_codec_h264=True) + return f"rtsp://{self.device.username}:{self.device.password}@{self.device.host}/axis-media/media.amp{options}" - return f"rtsp://{self.device.config_entry.data[CONF_USERNAME]}:{self.device.config_entry.data[CONF_PASSWORD]}@{self.device.host}/axis-media/media.amp?videocodec=h264{options}" + def generate_options( + self, skip_stream_profile: bool = False, add_video_codec_h264: bool = False + ) -> str: + """Generate options for video stream.""" + options_dict = {} + + if add_video_codec_h264: + options_dict["videocodec"] = "h264" + + if ( + not skip_stream_profile + and self.device.option_stream_profile != DEFAULT_STREAM_PROFILE + ): + options_dict["streamprofile"] = self.device.option_stream_profile + + if self.device.option_video_source != DEFAULT_VIDEO_SOURCE: + options_dict["camera"] = self.device.option_video_source + + if not options_dict: + return "" + return f"?{urlencode(options_dict)}" diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 8d52b7f8d9f..d99c5329e32 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -1,10 +1,12 @@ """Config flow to configure Axis devices.""" from ipaddress import ip_address +from urllib.parse import urlsplit import voluptuous as vol from homeassistant import config_entries +from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.config_entries import SOURCE_IGNORE from homeassistant.const import ( CONF_HOST, @@ -15,34 +17,28 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import callback +from homeassistant.helpers.device_registry import format_mac from homeassistant.util.network import is_link_local from .const import ( CONF_MODEL, CONF_STREAM_PROFILE, + CONF_VIDEO_SOURCE, DEFAULT_STREAM_PROFILE, + DEFAULT_VIDEO_SOURCE, DOMAIN as AXIS_DOMAIN, ) from .device import get_device from .errors import AuthenticationRequired, CannotConnect -AXIS_OUI = {"00408C", "ACCC8E", "B8A44F"} - -CONFIG_FILE = "axis.conf" - -EVENT_TYPES = ["motion", "vmd3", "pir", "sound", "daynight", "tampering", "input"] - -PLATFORMS = ["camera"] - -AXIS_INCLUDE = EVENT_TYPES + PLATFORMS - +AXIS_OUI = {"00:40:8c", "ac:cc:8e", "b8:a4:4f"} DEFAULT_PORT = 80 class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): """Handle a Axis config flow.""" - VERSION = 2 + VERSION = 3 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH @staticmethod @@ -56,6 +52,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): self.device_config = {} self.discovery_schema = {} self.import_schema = {} + self.serial = None async def async_step_user(self, user_input=None): """Handle a Axis config flow start. @@ -74,12 +71,15 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): password=user_input[CONF_PASSWORD], ) - await self.async_set_unique_id(device.vapix.serial_number) + self.serial = device.vapix.serial_number + await self.async_set_unique_id(format_mac(self.serial)) self._abort_if_unique_id_configured( updates={ CONF_HOST: user_input[CONF_HOST], CONF_PORT: user_input[CONF_PORT], + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], } ) @@ -88,7 +88,6 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): CONF_PORT: user_input[CONF_PORT], CONF_USERNAME: user_input[CONF_USERNAME], CONF_PASSWORD: user_input[CONF_PASSWORD], - CONF_MAC: device.vapix.serial_number, CONF_MODEL: device.vapix.product_number, } @@ -134,39 +133,88 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=AXIS_DOMAIN): self.device_config[CONF_NAME] = name - title = f"{model} - {self.device_config[CONF_MAC]}" + title = f"{model} - {self.serial}" return self.async_create_entry(title=title, data=self.device_config) - async def async_step_zeroconf(self, discovery_info): - """Prepare configuration for a discovered Axis device.""" - serial_number = discovery_info["properties"]["macaddress"] + async def async_step_reauth(self, device_config: dict): + """Trigger a reauthentication flow.""" + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context["title_placeholders"] = { + CONF_NAME: device_config[CONF_NAME], + CONF_HOST: device_config[CONF_HOST], + } - if serial_number[:6] not in AXIS_OUI: + self.discovery_schema = { + vol.Required(CONF_HOST, default=device_config[CONF_HOST]): str, + vol.Required(CONF_USERNAME, default=device_config[CONF_USERNAME]): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_PORT, default=device_config[CONF_PORT]): int, + } + + return await self.async_step_user() + + async def async_step_dhcp(self, discovery_info: dict): + """Prepare configuration for a DHCP discovered Axis device.""" + return await self._process_discovered_device( + { + CONF_HOST: discovery_info[IP_ADDRESS], + CONF_MAC: format_mac(discovery_info.get(MAC_ADDRESS)), + CONF_NAME: discovery_info.get(HOSTNAME), + CONF_PORT: DEFAULT_PORT, + } + ) + + async def async_step_ssdp(self, discovery_info: dict): + """Prepare configuration for a SSDP discovered Axis device.""" + url = urlsplit(discovery_info["presentationURL"]) + return await self._process_discovered_device( + { + CONF_HOST: url.hostname, + CONF_MAC: format_mac(discovery_info["serialNumber"]), + CONF_NAME: f"{discovery_info['friendlyName']}", + CONF_PORT: url.port, + } + ) + + async def async_step_zeroconf(self, discovery_info: dict): + """Prepare configuration for a Zeroconf discovered Axis device.""" + return await self._process_discovered_device( + { + CONF_HOST: discovery_info[CONF_HOST], + CONF_MAC: format_mac(discovery_info["properties"]["macaddress"]), + CONF_NAME: discovery_info["name"].split(".", 1)[0], + CONF_PORT: discovery_info[CONF_PORT], + } + ) + + async def _process_discovered_device(self, device: dict): + """Prepare configuration for a discovered Axis device.""" + if device[CONF_MAC][:8] not in AXIS_OUI: return self.async_abort(reason="not_axis_device") - if is_link_local(ip_address(discovery_info[CONF_HOST])): + if is_link_local(ip_address(device[CONF_HOST])): return self.async_abort(reason="link_local_address") - await self.async_set_unique_id(serial_number) + await self.async_set_unique_id(device[CONF_MAC]) self._abort_if_unique_id_configured( updates={ - CONF_HOST: discovery_info[CONF_HOST], - CONF_PORT: discovery_info[CONF_PORT], + CONF_HOST: device[CONF_HOST], + CONF_PORT: device[CONF_PORT], } ) # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context["title_placeholders"] = { - CONF_NAME: discovery_info["hostname"][:-7], - CONF_HOST: discovery_info[CONF_HOST], + CONF_NAME: device[CONF_NAME], + CONF_HOST: device[CONF_HOST], } self.discovery_schema = { - vol.Required(CONF_HOST, default=discovery_info[CONF_HOST]): str, + vol.Required(CONF_HOST, default=device[CONF_HOST]): str, vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, - vol.Required(CONF_PORT, default=discovery_info[CONF_PORT]): int, + vol.Required(CONF_PORT, default=device[CONF_PORT]): int, } return await self.async_step_user() @@ -187,22 +235,44 @@ class AxisOptionsFlowHandler(config_entries.OptionsFlow): return await self.async_step_configure_stream() async def async_step_configure_stream(self, user_input=None): - """Manage the Axis device options.""" + """Manage the Axis device stream options.""" if user_input is not None: self.options.update(user_input) return self.async_create_entry(title="", data=self.options) - profiles = [DEFAULT_STREAM_PROFILE] - for profile in self.device.api.vapix.streaming_profiles: - profiles.append(profile.name) + schema = {} + + vapix = self.device.api.vapix + + # Stream profiles + + if vapix.params.stream_profiles_max_groups > 0: + + stream_profiles = [DEFAULT_STREAM_PROFILE] + for profile in vapix.streaming_profiles: + stream_profiles.append(profile.name) + + schema[ + vol.Optional( + CONF_STREAM_PROFILE, default=self.device.option_stream_profile + ) + ] = vol.In(stream_profiles) + + # Video sources + + if vapix.params.image_nbrofviews > 0: + await vapix.params.update_image() + + video_sources = {DEFAULT_VIDEO_SOURCE: DEFAULT_VIDEO_SOURCE} + for idx, video_source in vapix.params.image_sources.items(): + if not video_source["Enabled"]: + continue + video_sources[idx + 1] = video_source["Name"] + + schema[ + vol.Optional(CONF_VIDEO_SOURCE, default=self.device.option_video_source) + ] = vol.In(video_sources) return self.async_show_form( - step_id="configure_stream", - data_schema=vol.Schema( - { - vol.Optional( - CONF_STREAM_PROFILE, default=self.device.option_stream_profile - ): vol.In(profiles) - } - ), + step_id="configure_stream", data_schema=vol.Schema(schema) ) diff --git a/homeassistant/components/axis/const.py b/homeassistant/components/axis/const.py index 12a10391e4c..a1ce77f099b 100644 --- a/homeassistant/components/axis/const.py +++ b/homeassistant/components/axis/const.py @@ -15,9 +15,11 @@ ATTR_MANUFACTURER = "Axis Communications AB" CONF_EVENTS = "events" CONF_MODEL = "model" CONF_STREAM_PROFILE = "stream_profile" +CONF_VIDEO_SOURCE = "video_source" DEFAULT_EVENTS = True DEFAULT_STREAM_PROFILE = "No stream profile" DEFAULT_TRIGGER_TIME = 0 +DEFAULT_VIDEO_SOURCE = "No video source" PLATFORMS = [BINARY_SENSOR_DOMAIN, CAMERA_DOMAIN, LIGHT_DOMAIN, SWITCH_DOMAIN] diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index 79204bf3002..bd7b5e442ad 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -13,6 +13,7 @@ from axis.streammanager import SIGNAL_PLAYING, STATE_STOPPED from homeassistant.components import mqtt from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN from homeassistant.components.mqtt.models import Message +from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -25,6 +26,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.httpx_client import get_async_client from homeassistant.setup import async_when_setup from .const import ( @@ -32,9 +34,11 @@ from .const import ( CONF_EVENTS, CONF_MODEL, CONF_STREAM_PROFILE, + CONF_VIDEO_SOURCE, DEFAULT_EVENTS, DEFAULT_STREAM_PROFILE, DEFAULT_TRIGGER_TIME, + DEFAULT_VIDEO_SOURCE, DOMAIN as AXIS_DOMAIN, LOGGER, PLATFORMS, @@ -59,9 +63,24 @@ class AxisNetworkDevice: @property def host(self): - """Return the host of this device.""" + """Return the host address of this device.""" return self.config_entry.data[CONF_HOST] + @property + def port(self): + """Return the HTTP port of this device.""" + return self.config_entry.data[CONF_PORT] + + @property + def username(self): + """Return the username of this device.""" + return self.config_entry.data[CONF_USERNAME] + + @property + def password(self): + """Return the password of this device.""" + return self.config_entry.data[CONF_PASSWORD] + @property def model(self): """Return the model of this device.""" @@ -73,8 +92,8 @@ class AxisNetworkDevice: return self.config_entry.data[CONF_NAME] @property - def serial(self): - """Return the serial number of this device.""" + def unique_id(self): + """Return the unique ID (serial number) of this device.""" return self.config_entry.unique_id # Options @@ -96,22 +115,27 @@ class AxisNetworkDevice: """Config entry option defining minimum number of seconds to keep trigger high.""" return self.config_entry.options.get(CONF_TRIGGER_TIME, DEFAULT_TRIGGER_TIME) + @property + def option_video_source(self): + """Config entry option defining what video source camera platform should use.""" + return self.config_entry.options.get(CONF_VIDEO_SOURCE, DEFAULT_VIDEO_SOURCE) + # Signals @property def signal_reachable(self): """Device specific event to signal a change in connection status.""" - return f"axis_reachable_{self.serial}" + return f"axis_reachable_{self.unique_id}" @property def signal_new_event(self): """Device specific event to signal new device event available.""" - return f"axis_new_event_{self.serial}" + return f"axis_new_event_{self.unique_id}" @property def signal_new_address(self): """Device specific event to signal a change in device address.""" - return f"axis_new_address_{self.serial}" + return f"axis_new_address_{self.unique_id}" # Callbacks @@ -150,8 +174,8 @@ class AxisNetworkDevice: device_registry = await self.hass.helpers.device_registry.async_get_registry() device_registry.async_get_or_create( config_entry_id=self.config_entry.entry_id, - connections={(CONNECTION_NETWORK_MAC, self.serial)}, - identifiers={(AXIS_DOMAIN, self.serial)}, + connections={(CONNECTION_NETWORK_MAC, self.unique_id)}, + identifiers={(AXIS_DOMAIN, self.unique_id)}, manufacturer=ATTR_MANUFACTURER, model=f"{self.model} {self.product_type}", name=self.name, @@ -168,7 +192,9 @@ class AxisNetworkDevice: if status.get("data", {}).get("status", {}).get("state") == "active": self.listeners.append( - await mqtt.async_subscribe(hass, f"{self.serial}/#", self.mqtt_message) + await mqtt.async_subscribe( + hass, f"{self.api.vapix.serial_number}/#", self.mqtt_message + ) ) @callback @@ -177,7 +203,7 @@ class AxisNetworkDevice: self.disconnect_from_stream() event = mqtt_json_to_event(message.payload) - self.api.event.process_event(event) + self.api.event.update([event]) # Setup and teardown methods @@ -186,17 +212,23 @@ class AxisNetworkDevice: try: self.api = await get_device( self.hass, - host=self.config_entry.data[CONF_HOST], - port=self.config_entry.data[CONF_PORT], - username=self.config_entry.data[CONF_USERNAME], - password=self.config_entry.data[CONF_PASSWORD], + host=self.host, + port=self.port, + username=self.username, + password=self.password, ) except CannotConnect as err: raise ConfigEntryNotReady from err - except Exception: # pylint: disable=broad-except - LOGGER.error("Unknown error connecting with Axis device on %s", self.host) + except AuthenticationRequired: + self.hass.async_create_task( + self.hass.config_entries.flow.async_init( + AXIS_DOMAIN, + context={"source": SOURCE_REAUTH}, + data=self.config_entry.data, + ) + ) return False self.fw_version = self.api.vapix.firmware_version @@ -239,12 +271,10 @@ class AxisNetworkDevice: async def shutdown(self, event): """Stop the event stream.""" self.disconnect_from_stream() - await self.api.vapix.close() async def async_reset(self): """Reset this device to default state.""" self.disconnect_from_stream() - await self.api.vapix.close() unload_ok = all( await asyncio.gather( @@ -267,9 +297,10 @@ class AxisNetworkDevice: async def get_device(hass, host, port, username, password): """Create a Axis device.""" + session = get_async_client(hass, verify_ssl=False) device = axis.AxisDevice( - Configuration(host, port=port, username=username, password=password) + Configuration(session, host, port=port, username=username, password=password) ) try: @@ -280,15 +311,12 @@ async def get_device(hass, host, port, username, password): except axis.Unauthorized as err: LOGGER.warning("Connected to device at %s but not registered.", host) - await device.vapix.close() raise AuthenticationRequired from err except (asyncio.TimeoutError, axis.RequestError) as err: LOGGER.error("Error connecting to the Axis device at %s", host) - await device.vapix.close() raise CannotConnect from err except axis.AxisException as err: LOGGER.exception("Unknown Axis communication error occurred") - await device.vapix.close() raise AuthenticationRequired from err diff --git a/homeassistant/components/axis/light.py b/homeassistant/components/axis/light.py index df4c00415ff..75a42b13cbf 100644 --- a/homeassistant/components/axis/light.py +++ b/homeassistant/components/axis/light.py @@ -18,7 +18,10 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up a Axis light.""" device = hass.data[AXIS_DOMAIN][config_entry.unique_id] - if not device.api.vapix.light_control: + if ( + device.api.vapix.light_control is None + or len(device.api.vapix.light_control) == 0 + ): return @callback diff --git a/homeassistant/components/axis/manifest.json b/homeassistant/components/axis/manifest.json index 188520241f3..a78d916da9e 100644 --- a/homeassistant/components/axis/manifest.json +++ b/homeassistant/components/axis/manifest.json @@ -3,12 +3,23 @@ "name": "Axis", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/axis", - "requirements": ["axis==41"], + "requirements": ["axis==43"], + "dhcp": [ + { "hostname": "axis-00408c*", "macaddress": "00408C*" }, + { "hostname": "axis-accc8e*", "macaddress": "ACCC8E*" }, + { "hostname": "axis-b8a44f*", "macaddress": "B8A44F*" } + ], + "ssdp": [ + { + "manufacturer": "AXIS" + } + ], "zeroconf": [ { "type": "_axis-video._tcp.local.", "macaddress": "00408C*" }, { "type": "_axis-video._tcp.local.", "macaddress": "ACCC8E*" }, { "type": "_axis-video._tcp.local.", "macaddress": "B8A44F*" } ], "after_dependencies": ["mqtt"], - "codeowners": ["@Kane610"] + "codeowners": ["@Kane610"], + "quality_scale": "platinum" } diff --git a/homeassistant/components/axis/strings.json b/homeassistant/components/axis/strings.json index 046b7fee475..47a25b542a7 100644 --- a/homeassistant/components/axis/strings.json +++ b/homeassistant/components/axis/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "Axis device: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "title": "Set up Axis device", diff --git a/homeassistant/components/axis/translations/ca.json b/homeassistant/components/axis/translations/ca.json index 26da6057dc1..3e104c1005e 100644 --- a/homeassistant/components/axis/translations/ca.json +++ b/homeassistant/components/axis/translations/ca.json @@ -11,7 +11,7 @@ "cannot_connect": "Ha fallat la connexi\u00f3", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" }, - "flow_title": "Dispositiu d'eix: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/translations/cs.json b/homeassistant/components/axis/translations/cs.json index fd99c68ab35..4f7c3016235 100644 --- a/homeassistant/components/axis/translations/cs.json +++ b/homeassistant/components/axis/translations/cs.json @@ -11,7 +11,7 @@ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" }, - "flow_title": "Za\u0159\u00edzen\u00ed Axis: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/translations/de.json b/homeassistant/components/axis/translations/de.json index 4706350cdb3..1f6aedf5d9c 100644 --- a/homeassistant/components/axis/translations/de.json +++ b/homeassistant/components/axis/translations/de.json @@ -7,8 +7,9 @@ }, "error": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "already_in_progress": "Der Konfigurationsablauf f\u00fcr das Ger\u00e4t wird bereits ausgef\u00fchrt.", - "cannot_connect": "Verbindungsfehler" + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "flow_title": "Achsenger\u00e4t: {name} ({host})", "step": { diff --git a/homeassistant/components/axis/translations/en.json b/homeassistant/components/axis/translations/en.json index 6b01533aefa..f71e91f6280 100644 --- a/homeassistant/components/axis/translations/en.json +++ b/homeassistant/components/axis/translations/en.json @@ -11,7 +11,7 @@ "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication" }, - "flow_title": "Axis device: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/translations/et.json b/homeassistant/components/axis/translations/et.json index 6a27e74b287..f6f9a523cb6 100644 --- a/homeassistant/components/axis/translations/et.json +++ b/homeassistant/components/axis/translations/et.json @@ -11,7 +11,7 @@ "cannot_connect": "\u00dchendamine nurjus", "invalid_auth": "Tuvastamise viga" }, - "flow_title": "Axise seade: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/translations/it.json b/homeassistant/components/axis/translations/it.json index 6461b2a6619..7e7aeb1d1b2 100644 --- a/homeassistant/components/axis/translations/it.json +++ b/homeassistant/components/axis/translations/it.json @@ -11,7 +11,7 @@ "cannot_connect": "Impossibile connettersi", "invalid_auth": "Autenticazione non valida" }, - "flow_title": "Dispositivo Axis: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/translations/no.json b/homeassistant/components/axis/translations/no.json index 984d522eba9..1fc0640eb9b 100644 --- a/homeassistant/components/axis/translations/no.json +++ b/homeassistant/components/axis/translations/no.json @@ -11,7 +11,7 @@ "cannot_connect": "Tilkobling mislyktes", "invalid_auth": "Ugyldig godkjenning" }, - "flow_title": "Axis enhet: {name} ({host})", + "flow_title": "{name} ( {host} )", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/translations/pl.json b/homeassistant/components/axis/translations/pl.json index 84af845ab31..e44816bc2ea 100644 --- a/homeassistant/components/axis/translations/pl.json +++ b/homeassistant/components/axis/translations/pl.json @@ -11,7 +11,7 @@ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "invalid_auth": "Niepoprawne uwierzytelnienie" }, - "flow_title": "Urz\u0105dzenie Axis: {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/translations/ru.json b/homeassistant/components/axis/translations/ru.json index ee1dc8494f3..6d979dc9de0 100644 --- a/homeassistant/components/axis/translations/ru.json +++ b/homeassistant/components/axis/translations/ru.json @@ -11,7 +11,7 @@ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." }, - "flow_title": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Axis {name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/axis/translations/tr.json b/homeassistant/components/axis/translations/tr.json new file mode 100644 index 00000000000..b2d609747d1 --- /dev/null +++ b/homeassistant/components/axis/translations/tr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/translations/uk.json b/homeassistant/components/axis/translations/uk.json new file mode 100644 index 00000000000..35b849ce968 --- /dev/null +++ b/homeassistant/components/axis/translations/uk.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "link_local_address": "\u041f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u0456 \u0430\u0434\u0440\u0435\u0441\u0438 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f.", + "not_axis_device": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0454 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c Axis." + }, + "error": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "flow_title": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 Axis {name} ({host})", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "Axis" + } + } + }, + "options": { + "step": { + "configure_stream": { + "data": { + "stream_profile": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u043e\u0444\u0456\u043b\u044c \u043f\u043e\u0442\u043e\u043a\u0443 \u0434\u043b\u044f \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f" + }, + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0432\u0456\u0434\u0435\u043e\u043f\u043e\u0442\u043e\u043a\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Axis" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/translations/zh-Hant.json b/homeassistant/components/axis/translations/zh-Hant.json index 1d7aaa7c74e..293f08c5f05 100644 --- a/homeassistant/components/axis/translations/zh-Hant.json +++ b/homeassistant/components/axis/translations/zh-Hant.json @@ -11,7 +11,7 @@ "cannot_connect": "\u9023\u7dda\u5931\u6557", "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" }, - "flow_title": "Axis \u88dd\u7f6e\uff1a{name} ({host})", + "flow_title": "{name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/azure_devops/translations/de.json b/homeassistant/components/azure_devops/translations/de.json index 1c940ea7a35..e7d9e073ec6 100644 --- a/homeassistant/components/azure_devops/translations/de.json +++ b/homeassistant/components/azure_devops/translations/de.json @@ -1,7 +1,12 @@ { "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" + }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { "reauth": { diff --git a/homeassistant/components/azure_devops/translations/fr.json b/homeassistant/components/azure_devops/translations/fr.json index edcf3dda517..5e62d54ec1d 100644 --- a/homeassistant/components/azure_devops/translations/fr.json +++ b/homeassistant/components/azure_devops/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9", + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", "reauth_successful": "Jeton d'acc\u00e8s mis \u00e0 jour avec succ\u00e8s" }, "error": { diff --git a/homeassistant/components/azure_devops/translations/tr.json b/homeassistant/components/azure_devops/translations/tr.json new file mode 100644 index 00000000000..11a15956f63 --- /dev/null +++ b/homeassistant/components/azure_devops/translations/tr.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "project_error": "Proje bilgileri al\u0131namad\u0131." + }, + "flow_title": "Azure DevOps: {project_url}", + "step": { + "reauth": { + "title": "Yeniden kimlik do\u011frulama" + }, + "user": { + "data": { + "organization": "Organizasyon", + "personal_access_token": "Ki\u015fisel Eri\u015fim Belirteci (PAT)", + "project": "Proje" + }, + "description": "Projenize eri\u015fmek i\u00e7in bir Azure DevOps \u00f6rne\u011fi ayarlay\u0131n. Ki\u015fisel Eri\u015fim Jetonu yaln\u0131zca \u00f6zel bir proje i\u00e7in gereklidir.", + "title": "Azure DevOps Projesi Ekle" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/azure_devops/translations/uk.json b/homeassistant/components/azure_devops/translations/uk.json index 4a42fd17fc3..848528f444e 100644 --- a/homeassistant/components/azure_devops/translations/uk.json +++ b/homeassistant/components/azure_devops/translations/uk.json @@ -1,16 +1,30 @@ { "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "project_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u043f\u0440\u043e\u0435\u043a\u0442." + }, "flow_title": "Azure DevOps: {project_url}", "step": { "reauth": { + "data": { + "personal_access_token": "\u041f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443 (PAT)" + }, + "description": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457 {project_url} . \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0456 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0456 \u0434\u0430\u043d\u0456.", "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f" }, "user": { "data": { "organization": "\u041e\u0440\u0433\u0430\u043d\u0456\u0437\u0430\u0446\u0456\u044f", "personal_access_token": "\u0422\u043e\u043a\u0435\u043d \u043e\u0441\u043e\u0431\u0438\u0441\u0442\u043e\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0443 (PAT)", - "project": "\u041f\u0440\u043e\u0454\u043a\u0442" + "project": "\u041f\u0440\u043e\u0435\u043a\u0442" }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 \u043f\u0440\u043e\u0435\u043a\u0442\u043e\u043c Azure DevOps. \u041f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0432\u0432\u043e\u0434\u0438\u0442\u0438 \u043b\u0438\u0448\u0435 \u0434\u043b\u044f \u043f\u0440\u0438\u0432\u0430\u0442\u043d\u0438\u0445 \u043f\u0440\u043e\u0435\u043a\u0442\u0456\u0432.", "title": "\u0414\u043e\u0434\u0430\u0442\u0438 \u043f\u0440\u043e\u0435\u043a\u0442 Azure DevOps" } } diff --git a/homeassistant/components/binary_sensor/translations/de.json b/homeassistant/components/binary_sensor/translations/de.json index 3687536eb5b..a78befb7965 100644 --- a/homeassistant/components/binary_sensor/translations/de.json +++ b/homeassistant/components/binary_sensor/translations/de.json @@ -98,6 +98,10 @@ "off": "Normal", "on": "Schwach" }, + "battery_charging": { + "off": "L\u00e4dt nicht", + "on": "L\u00e4dt" + }, "cold": { "off": "Normal", "on": "Kalt" @@ -122,6 +126,10 @@ "off": "Normal", "on": "Hei\u00df" }, + "light": { + "off": "Kein Licht", + "on": "Licht erkannt" + }, "lock": { "off": "Verriegelt", "on": "Entriegelt" @@ -134,6 +142,10 @@ "off": "Ruhig", "on": "Bewegung erkannt" }, + "moving": { + "off": "Bewegt sich nicht", + "on": "Bewegt sich" + }, "occupancy": { "off": "Frei", "on": "Belegt" @@ -142,6 +154,10 @@ "off": "Geschlossen", "on": "Offen" }, + "plug": { + "off": "Ausgesteckt", + "on": "Eingesteckt" + }, "presence": { "off": "Abwesend", "on": "Zu Hause" diff --git a/homeassistant/components/binary_sensor/translations/pl.json b/homeassistant/components/binary_sensor/translations/pl.json index 7d6e8eab4ba..726765aea02 100644 --- a/homeassistant/components/binary_sensor/translations/pl.json +++ b/homeassistant/components/binary_sensor/translations/pl.json @@ -99,7 +99,7 @@ "on": "roz\u0142adowana" }, "battery_charging": { - "off": "nie \u0142aduje", + "off": "roz\u0142adowywanie", "on": "\u0142adowanie" }, "cold": { diff --git a/homeassistant/components/binary_sensor/translations/tr.json b/homeassistant/components/binary_sensor/translations/tr.json index 3c5cfaeeacf..94e1496cc30 100644 --- a/homeassistant/components/binary_sensor/translations/tr.json +++ b/homeassistant/components/binary_sensor/translations/tr.json @@ -1,4 +1,10 @@ { + "device_automation": { + "trigger_type": { + "moist": "{entity_name} nemli oldu", + "not_opened": "{entity_name} kapat\u0131ld\u0131" + } + }, "state": { "_": { "off": "Kapal\u0131", @@ -8,6 +14,10 @@ "off": "Normal", "on": "D\u00fc\u015f\u00fck" }, + "battery_charging": { + "off": "\u015earj olmuyor", + "on": "\u015earj Oluyor" + }, "cold": { "off": "Normal", "on": "So\u011fuk" @@ -32,6 +42,10 @@ "off": "Normal", "on": "S\u0131cak" }, + "light": { + "off": "I\u015f\u0131k yok", + "on": "I\u015f\u0131k alg\u0131land\u0131" + }, "lock": { "off": "Kilit kapal\u0131", "on": "Kilit a\u00e7\u0131k" @@ -44,6 +58,10 @@ "off": "Temiz", "on": "Alg\u0131land\u0131" }, + "moving": { + "off": "Hareket etmiyor", + "on": "Hareketli" + }, "occupancy": { "off": "Temiz", "on": "Alg\u0131land\u0131" @@ -52,6 +70,10 @@ "off": "Kapal\u0131", "on": "A\u00e7\u0131k" }, + "plug": { + "off": "Fi\u015fi \u00e7ekildi", + "on": "Tak\u0131l\u0131" + }, "presence": { "off": "[%key:common::state::evde_degil%]", "on": "[%key:common::state::evde%]" diff --git a/homeassistant/components/binary_sensor/translations/uk.json b/homeassistant/components/binary_sensor/translations/uk.json index 29767f6d6d6..0f8d92749c4 100644 --- a/homeassistant/components/binary_sensor/translations/uk.json +++ b/homeassistant/components/binary_sensor/translations/uk.json @@ -2,15 +2,91 @@ "device_automation": { "condition_type": { "is_bat_low": "{entity_name} \u043d\u0438\u0437\u044c\u043a\u0438\u0439 \u0440\u0456\u0432\u0435\u043d\u044c \u0430\u043a\u0443\u043c\u0443\u043b\u044f\u0442\u043e\u0440\u0430", - "is_not_bat_low": "{entity_name} \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u0438\u043c \u0437\u0430\u0440\u044f\u0434 \u0430\u043a\u0443\u043c\u0443\u043b\u044f\u0442\u043e\u0440\u0430" + "is_cold": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f", + "is_connected": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "is_gas": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0433\u0430\u0437", + "is_hot": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043d\u0430\u0433\u0440\u0456\u0432", + "is_light": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0441\u0432\u0456\u0442\u043b\u043e", + "is_locked": "{entity_name} \u0432 \u0437\u0430\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_moist": "{entity_name} \u0432 \u0441\u0442\u0430\u043d\u0456 \"\u0412\u043e\u043b\u043e\u0433\u043e\"", + "is_motion": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0440\u0443\u0445", + "is_moving": "{entity_name} \u043f\u0435\u0440\u0435\u043c\u0456\u0449\u0443\u0454\u0442\u044c\u0441\u044f", + "is_no_gas": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0433\u0430\u0437", + "is_no_light": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0441\u0432\u0456\u0442\u043b\u043e", + "is_no_motion": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0440\u0443\u0445", + "is_no_problem": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u043e\u0431\u043b\u0435\u043c", + "is_no_smoke": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0434\u0438\u043c", + "is_no_sound": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0437\u0432\u0443\u043a", + "is_no_vibration": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0432\u0456\u0431\u0440\u0430\u0446\u0456\u044e", + "is_not_bat_low": "{entity_name} \u0432 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_not_cold": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f", + "is_not_connected": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "is_not_hot": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043d\u0430\u0433\u0440\u0456\u0432", + "is_not_locked": "{entity_name} \u0432 \u0440\u043e\u0437\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_not_moist": "{entity_name} \u0432 \u0441\u0442\u0430\u043d\u0456 \"\u0421\u0443\u0445\u043e\"", + "is_not_moving": "{entity_name} \u043d\u0435 \u0440\u0443\u0445\u0430\u0454\u0442\u044c\u0441\u044f", + "is_not_occupied": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c", + "is_not_open": "{entity_name} \u0432 \u0437\u0430\u043a\u0440\u0438\u0442\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_not_plugged_in": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "is_not_powered": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0436\u0438\u0432\u043b\u0435\u043d\u043d\u044f", + "is_not_present": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c", + "is_not_unsafe": "{entity_name} \u0432 \u0431\u0435\u0437\u043f\u0435\u0447\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_occupied": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c", + "is_off": "{entity_name} \u0443 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_on": "{entity_name} \u0443 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_open": "{entity_name} \u0443 \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_plugged_in": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "is_powered": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0436\u0438\u0432\u043b\u0435\u043d\u043d\u044f", + "is_present": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c", + "is_problem": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", + "is_smoke": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0434\u0438\u043c", + "is_sound": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0437\u0432\u0443\u043a", + "is_unsafe": "{entity_name} \u0432 \u043d\u0435\u0431\u0435\u0437\u043f\u0435\u0447\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_vibration": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0432\u0456\u0431\u0440\u0430\u0446\u0456\u044e" }, "trigger_type": { - "bat_low": "{entity_name} \u043d\u0438\u0437\u044c\u043a\u0438\u0439 \u0437\u0430\u0440\u044f\u0434 \u0430\u043a\u0443\u043c\u0443\u043b\u044f\u0442\u043e\u0440\u0430", - "not_bat_low": "{entity_name} \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u0438\u0439 \u0430\u043a\u0443\u043c\u0443\u043b\u044f\u0442\u043e\u0440", + "bat_low": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u043d\u0438\u0437\u044c\u043a\u0438\u0439 \u0437\u0430\u0440\u044f\u0434", + "cold": "{entity_name} \u043e\u0445\u043e\u043b\u043e\u0434\u0436\u0443\u0454\u0442\u044c\u0441\u044f", + "connected": "{entity_name} \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0430\u0454\u0442\u044c\u0441\u044f", + "gas": "{entity_name} \u043f\u043e\u0447\u0438\u043d\u0430\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0433\u0430\u0437", + "hot": "{entity_name} \u043d\u0430\u0433\u0440\u0456\u0432\u0430\u0454\u0442\u044c\u0441\u044f", + "light": "{entity_name} \u043f\u043e\u0447\u0438\u043d\u0430\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0441\u0432\u0456\u0442\u043b\u043e", + "locked": "{entity_name} \u0431\u043b\u043e\u043a\u0443\u0454\u0442\u044c\u0441\u044f", + "moist": "{entity_name} \u0441\u0442\u0430\u0454 \u0432\u043e\u043b\u043e\u0433\u0438\u043c", + "motion": "{entity_name} \u043f\u043e\u0447\u0438\u043d\u0430\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0440\u0443\u0445", + "moving": "{entity_name} \u043f\u043e\u0447\u0438\u043d\u0430\u0454 \u0440\u0443\u0445\u0430\u0442\u0438\u0441\u044f", + "no_gas": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0433\u0430\u0437", + "no_light": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0441\u0432\u0456\u0442\u043b\u043e", + "no_motion": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0440\u0443\u0445", + "no_problem": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", + "no_smoke": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0434\u0438\u043c", + "no_sound": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0437\u0432\u0443\u043a", + "no_vibration": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u0432\u0438\u044f\u0432\u043b\u044f\u0442\u0438 \u0432\u0456\u0431\u0440\u0430\u0446\u0456\u044e", + "not_bat_low": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u043d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u0438\u0439 \u0437\u0430\u0440\u044f\u0434", + "not_cold": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u043e\u0445\u043e\u043b\u043e\u0434\u0436\u0443\u0432\u0430\u0442\u0438\u0441\u044f", + "not_connected": "{entity_name} \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0430\u0454\u0442\u044c\u0441\u044f", + "not_hot": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u043d\u0430\u0433\u0440\u0456\u0432\u0430\u0442\u0438\u0441\u044f", + "not_locked": "{entity_name} \u0440\u043e\u0437\u0431\u043b\u043e\u043a\u0443\u0454\u0442\u044c\u0441\u044f", + "not_moist": "{entity_name} \u0441\u0442\u0430\u0454 \u0441\u0443\u0445\u0438\u043c", + "not_moving": "{entity_name} \u043f\u0440\u0438\u043f\u0438\u043d\u044f\u0454 \u043f\u0435\u0440\u0435\u043c\u0456\u0449\u0435\u043d\u043d\u044f", + "not_occupied": "{entity_name} \u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c", "not_opened": "{entity_name} \u0437\u0430\u043a\u0440\u0438\u0442\u043e", + "not_plugged_in": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "not_powered": "{entity_name} \u043d\u0435 \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u043d\u0430\u044f\u0432\u043d\u0456\u0441\u0442\u044c \u0436\u0438\u0432\u043b\u0435\u043d\u043d\u044f", + "not_present": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u0432\u0456\u0434\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c", + "not_unsafe": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u0431\u0435\u0437\u043f\u0435\u043a\u0443", + "occupied": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c", "opened": "{entity_name} \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u043e", + "plugged_in": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "powered": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u043d\u0430\u044f\u0432\u043d\u0456\u0441\u0442\u044c \u0436\u0438\u0432\u043b\u0435\u043d\u043d\u044f", + "present": "{entity_name} \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c", + "problem": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443", + "smoke": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0434\u0438\u043c", + "sound": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0437\u0432\u0443\u043a", "turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e", - "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e" + "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e", + "unsafe": "{entity_name} \u043d\u0435 \u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0454 \u0431\u0435\u0437\u043f\u0435\u043a\u0443", + "vibration": "{entity_name} \u0432\u0438\u044f\u0432\u043b\u044f\u0454 \u0432\u0456\u0431\u0440\u0430\u0446\u0456\u044e" } }, "state": { @@ -22,6 +98,10 @@ "off": "\u041d\u043e\u0440\u043c\u0430\u043b\u044c\u043d\u0438\u0439", "on": "\u041d\u0438\u0437\u044c\u043a\u0438\u0439" }, + "battery_charging": { + "off": "\u041d\u0435 \u0437\u0430\u0440\u044f\u0434\u0436\u0430\u0454\u0442\u044c\u0441\u044f", + "on": "\u0417\u0430\u0440\u044f\u0434\u0436\u0430\u043d\u043d\u044f" + }, "cold": { "off": "\u041d\u043e\u0440\u043c\u0430", "on": "\u041e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f" @@ -46,6 +126,10 @@ "off": "\u041d\u043e\u0440\u043c\u0430", "on": "\u041d\u0430\u0433\u0440\u0456\u0432\u0430\u043d\u043d\u044f" }, + "light": { + "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e", + "on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e" + }, "lock": { "off": "\u0417\u0430\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e", "on": "\u0420\u043e\u0437\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e" @@ -58,13 +142,21 @@ "off": "\u041d\u0435\u043c\u0430\u0454 \u0440\u0443\u0445\u0443", "on": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u0440\u0443\u0445" }, + "moving": { + "off": "\u0420\u0443\u0445\u0443 \u043d\u0435\u043c\u0430\u0454", + "on": "\u0420\u0443\u0445\u0430\u0454\u0442\u044c\u0441\u044f" + }, "occupancy": { "off": "\u0427\u0438\u0441\u0442\u043e", "on": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c" }, "opening": { - "off": "\u0417\u0430\u043a\u0440\u0438\u0442\u043e", - "on": "\u0412\u0456\u0434\u043a\u0440\u0438\u0442\u0438\u0439" + "off": "\u0417\u0430\u0447\u0438\u043d\u0435\u043d\u043e", + "on": "\u0412\u0456\u0434\u0447\u0438\u043d\u0435\u043d\u043e" + }, + "plug": { + "off": "\u0412\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "on": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e" }, "presence": { "off": "\u041d\u0435 \u0432\u0434\u043e\u043c\u0430", @@ -91,8 +183,8 @@ "on": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u0430 \u0432\u0456\u0431\u0440\u0430\u0446\u0456\u044f" }, "window": { - "off": "\u0417\u0430\u0447\u0438\u043d\u0435\u043d\u0435", - "on": "\u0412\u0456\u0434\u0447\u0438\u043d\u0435\u043d\u0435" + "off": "\u0417\u0430\u0447\u0438\u043d\u0435\u043d\u043e", + "on": "\u0412\u0456\u0434\u0447\u0438\u043d\u0435\u043d\u043e" } }, "title": "\u0411\u0456\u043d\u0430\u0440\u043d\u0438\u0439 \u0434\u0430\u0442\u0447\u0438\u043a" diff --git a/homeassistant/components/blebox/translations/de.json b/homeassistant/components/blebox/translations/de.json index baf14ba4897..37c8dde54e5 100644 --- a/homeassistant/components/blebox/translations/de.json +++ b/homeassistant/components/blebox/translations/de.json @@ -2,11 +2,11 @@ "config": { "abort": { "address_already_configured": "Ein BleBox-Ger\u00e4t ist bereits unter {address} konfiguriert.", - "already_configured": "Dieses BleBox-Ger\u00e4t ist bereits konfiguriert." + "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung mit dem BleBox-Ger\u00e4t nicht m\u00f6glich. (\u00dcberpr\u00fcfen Sie die Protokolle auf Fehler).", - "unknown": "Unbekannter Fehler beim Anschlie\u00dfen an das BleBox-Ger\u00e4t. (Pr\u00fcfen Sie die Protokolle auf Fehler).", + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler", "unsupported_version": "Das BleBox-Ger\u00e4t hat eine veraltete Firmware. Bitte aktualisieren Sie es zuerst." }, "flow_title": "BleBox-Ger\u00e4t: {name} ( {host} )", diff --git a/homeassistant/components/blebox/translations/tr.json b/homeassistant/components/blebox/translations/tr.json new file mode 100644 index 00000000000..31df3fb5e30 --- /dev/null +++ b/homeassistant/components/blebox/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "address_already_configured": "Bir BleBox cihaz\u0131 zaten {address} yap\u0131land\u0131r\u0131lm\u0131\u015ft\u0131r.", + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "\u0130p Adresi", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blebox/translations/uk.json b/homeassistant/components/blebox/translations/uk.json new file mode 100644 index 00000000000..fb10807acff --- /dev/null +++ b/homeassistant/components/blebox/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "address_already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u0437 \u0430\u0434\u0440\u0435\u0441\u043e\u044e {address} \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435.", + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430", + "unsupported_version": "\u041c\u0456\u043a\u0440\u043e\u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0430 \u0446\u044c\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u0437\u0430\u0441\u0442\u0430\u0440\u0456\u043b\u0430. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u043d\u043e\u0432\u0456\u0442\u044c \u0457\u0457." + }, + "flow_title": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 BleBox: {name} ({host})", + "step": { + "user": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 BleBox.", + "title": "BleBox" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/translations/de.json b/homeassistant/components/blink/translations/de.json index f5116110a09..d4f65329f9b 100644 --- a/homeassistant/components/blink/translations/de.json +++ b/homeassistant/components/blink/translations/de.json @@ -4,7 +4,8 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_access_token": "Ung\u00fcltiger Zugriffs-Token", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, @@ -13,7 +14,7 @@ "data": { "2fa": "Zwei-Faktor Authentifizierungscode" }, - "description": "Geben Sie die an Ihre E-Mail gesendete Pin ein. Wenn die E-Mail keine PIN enth\u00e4lt, lassen Sie das Feld leer.", + "description": "Gib die an deine E-Mail gesendete Pin ein. Wenn die E-Mail keine PIN enth\u00e4lt, lass das Feld leer.", "title": "Zwei-Faktor-Authentifizierung" }, "user": { diff --git a/homeassistant/components/blink/translations/tr.json b/homeassistant/components/blink/translations/tr.json new file mode 100644 index 00000000000..8193ff9d8be --- /dev/null +++ b/homeassistant/components/blink/translations/tr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_access_token": "Ge\u00e7ersiz eri\u015fim belirteci", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "2fa": { + "description": "E-postan\u0131za g\u00f6nderilen PIN kodunu girin" + }, + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/blink/translations/uk.json b/homeassistant/components/blink/translations/uk.json new file mode 100644 index 00000000000..c45bf7b6651 --- /dev/null +++ b/homeassistant/components/blink/translations/uk.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_access_token": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443.", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "2fa": { + "data": { + "2fa": "\u041a\u043e\u0434 \u0434\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0457 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c PIN-\u043a\u043e\u0434, \u043d\u0430\u0434\u0456\u0441\u043b\u0430\u043d\u0438\u0439 \u043d\u0430 \u0412\u0430\u0448\u0443 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0443 \u043f\u043e\u0448\u0442\u0443", + "title": "\u0414\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "Blink" + } + } + }, + "options": { + "step": { + "simple_options": { + "data": { + "scan_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u0441\u043a\u0430\u043d\u0443\u0432\u0430\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 Blink", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Blink" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bluetooth_le_tracker/device_tracker.py b/homeassistant/components/bluetooth_le_tracker/device_tracker.py index b2ea63abe69..76df4e65ac7 100644 --- a/homeassistant/components/bluetooth_le_tracker/device_tracker.py +++ b/homeassistant/components/bluetooth_le_tracker/device_tracker.py @@ -169,7 +169,7 @@ def setup_scanner(hass, config, see, discovery_info=None): ): handle = None try: - adapter.start(reset_on_start=True) + adapter.start(reset_on_start=False) _LOGGER.debug("Reading battery for Bluetooth LE device %s", mac) bt_device = adapter.connect(mac) # Try to get the handle; it will raise a BLEError exception if not available diff --git a/homeassistant/components/bmw_connected_drive/translations/ca.json b/homeassistant/components/bmw_connected_drive/translations/ca.json new file mode 100644 index 00000000000..d6bd70064c3 --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/ca.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "El compte ja ha estat configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "region": "Regi\u00f3 de ConnectedDrive", + "username": "Nom d'usuari" + } + } + } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "Nom\u00e9s de lectura (nom\u00e9s sensors i notificacions, sense execuci\u00f3 de serveis, sense bloqueig)", + "use_location": "Utilitza la ubicaci\u00f3 de Home Assistant per a les crides de localitzaci\u00f3 del cotxe (obligatori per a vehicles que no siguin i3/i8 produ\u00efts abans del 7/2014)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/cs.json b/homeassistant/components/bmw_connected_drive/translations/cs.json new file mode 100644 index 00000000000..665dccd443d --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/cs.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u00da\u010det je ji\u017e nastaven" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/de.json b/homeassistant/components/bmw_connected_drive/translations/de.json new file mode 100644 index 00000000000..12a870b4cc9 --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/en.json b/homeassistant/components/bmw_connected_drive/translations/en.json index f194c8a3444..dedd84d070b 100644 --- a/homeassistant/components/bmw_connected_drive/translations/en.json +++ b/homeassistant/components/bmw_connected_drive/translations/en.json @@ -11,7 +11,6 @@ "user": { "data": { "password": "Password", - "read_only": "Read-only", "region": "ConnectedDrive Region", "username": "Username" } diff --git a/homeassistant/components/bmw_connected_drive/translations/es.json b/homeassistant/components/bmw_connected_drive/translations/es.json new file mode 100644 index 00000000000..65ed9643f89 --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/es.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya est\u00e1 configurada" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "region": "Regi\u00f3n de ConnectedDrive", + "username": "Nombre de usuario" + } + } + } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "S\u00f3lo lectura (s\u00f3lo sensores y notificaci\u00f3n, sin ejecuci\u00f3n de servicios, sin bloqueo)", + "use_location": "Usar la ubicaci\u00f3n de Home Assistant para las encuestas de localizaci\u00f3n de autom\u00f3viles (necesario para los veh\u00edculos no i3/i8 producidos antes del 7/2014)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/et.json b/homeassistant/components/bmw_connected_drive/translations/et.json new file mode 100644 index 00000000000..f28209a1e7a --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/et.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Kasutaja on juba seadistatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Vigane autentimine" + }, + "step": { + "user": { + "data": { + "password": "Salas\u00f5na", + "region": "ConnectedDrive'i piirkond", + "username": "Kasutajanimi" + } + } + } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "Kirjutuskaitstud (ainult andurid ja teavitused, ei k\u00e4ivita teenuseid, kood puudub)", + "use_location": "Kasuta HA asukohta auto asukoha k\u00fcsitluste jaoks (n\u00f5utav enne 7/2014 toodetud muude kui i3 / i8 s\u00f5idukite jaoks)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/fr.json b/homeassistant/components/bmw_connected_drive/translations/fr.json new file mode 100644 index 00000000000..1b8f562669f --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/fr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "\u00c9chec \u00e0 la connexion", + "invalid_auth": "Authentification invalide" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "region": "R\u00e9gion ConnectedDrive", + "username": "Nom d'utilisateur" + } + } + } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "Lecture seule (uniquement capteurs et notification, pas d'ex\u00e9cution de services, pas de verrouillage)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/it.json b/homeassistant/components/bmw_connected_drive/translations/it.json new file mode 100644 index 00000000000..277ed189c43 --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/it.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "L'account \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida" + }, + "step": { + "user": { + "data": { + "password": "Password", + "region": "Regione ConnectedDrive", + "username": "Nome utente" + } + } + } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "Sola lettura (solo sensori e notifica, nessuna esecuzione di servizi, nessun blocco)", + "use_location": "Usa la posizione di Home Assistant per richieste sulla posizione dell'auto (richiesto per veicoli non i3/i8 prodotti prima del 7/2014)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/lb.json b/homeassistant/components/bmw_connected_drive/translations/lb.json new file mode 100644 index 00000000000..9ebbe919f8b --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/lb.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Kont ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "invalid_auth": "Ong\u00eblteg Authentifikatioun" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "region": "ConnectedDrive Regioun" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/no.json b/homeassistant/components/bmw_connected_drive/translations/no.json new file mode 100644 index 00000000000..f1715c550db --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/no.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Kontoen er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "region": "ConnectedDrive-region", + "username": "Brukernavn" + } + } + } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "Skrivebeskyttet (bare sensorer og varsler, ingen utf\u00f8relse av tjenester, ingen l\u00e5s)", + "use_location": "Bruk Home Assistant plassering for avstemningssteder for biler (p\u00e5krevd for ikke i3 / i8-kj\u00f8ret\u00f8y produsert f\u00f8r 7/2014)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/pl.json b/homeassistant/components/bmw_connected_drive/translations/pl.json new file mode 100644 index 00000000000..70467c6f9b9 --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/pl.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "region": "Region ConnectedDrive", + "username": "Nazwa u\u017cytkownika" + } + } + } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "Tylko odczyt (tylko czujniki i powiadomienia, brak wykonywania us\u0142ug, brak blokady)", + "use_location": "U\u017cyj lokalizacji Home Assistant do sondowania lokalizacji samochodu (wymagane w przypadku pojazd\u00f3w innych ni\u017c i3/i8 wyprodukowanych przed 7/2014)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/pt.json b/homeassistant/components/bmw_connected_drive/translations/pt.json new file mode 100644 index 00000000000..3814c892bd1 --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/pt.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Conta j\u00e1 configurada" + }, + "error": { + "cannot_connect": "Falha na liga\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + }, + "step": { + "user": { + "data": { + "password": "Palavra-passe", + "username": "Nome de Utilizador" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/ru.json b/homeassistant/components/bmw_connected_drive/translations/ru.json new file mode 100644 index 00000000000..0840affcef4 --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/ru.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "region": "\u0420\u0435\u0433\u0438\u043e\u043d ConnectedDrive", + "username": "\u041b\u043e\u0433\u0438\u043d" + } + } + } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "\u0422\u043e\u043b\u044c\u043a\u043e \u0447\u0442\u0435\u043d\u0438\u0435 (\u0442\u043e\u043b\u044c\u043a\u043e \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0438 \u0443\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u044f, \u0431\u0435\u0437 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f \u0441\u043b\u0443\u0436\u0431, \u0431\u0435\u0437 \u0431\u043b\u043e\u043a\u0438\u0440\u043e\u0432\u043a\u0438)", + "use_location": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 Home Assistant \u0434\u043b\u044f \u043e\u043f\u0440\u043e\u0441\u0430 \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u043e\u0431\u0438\u043b\u0435\u0439 (\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u043e\u0431\u0438\u043b\u0435\u0439 \u043d\u0435 i3/i8, \u0432\u044b\u043f\u0443\u0449\u0435\u043d\u043d\u044b\u0445 \u0434\u043e 7/2014)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/tr.json b/homeassistant/components/bmw_connected_drive/translations/tr.json new file mode 100644 index 00000000000..153aa4126b0 --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/uk.json b/homeassistant/components/bmw_connected_drive/translations/uk.json new file mode 100644 index 00000000000..68cdee2a66f --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/uk.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "region": "ConnectedDrive Region", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "\u041b\u0438\u0448\u0435 \u0434\u043b\u044f \u0447\u0438\u0442\u0430\u043d\u043d\u044f (\u043b\u0438\u0448\u0435 \u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0442\u0430 \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u043d\u044f, \u0431\u0435\u0437 \u0437\u0430\u043f\u0443\u0441\u043a\u0443 \u0441\u0435\u0440\u0432\u0456\u0441\u0456\u0432, \u0431\u0435\u0437 \u0431\u043b\u043e\u043a\u0443\u0432\u0430\u043d\u043d\u044f)", + "use_location": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f Home Assistant \u0434\u043b\u044f \u043e\u043f\u0438\u0442\u0443\u0432\u0430\u043d\u044c \u043c\u0456\u0441\u0446\u044f \u0440\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u043c\u043e\u0431\u0456\u043b\u0456\u0432 (\u043e\u0431\u043e\u0432\u2019\u044f\u0437\u043a\u043e\u0432\u043e \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u043e\u0431\u0456\u043b\u0456\u0432, \u0449\u043e \u043d\u0435 \u043d\u0430\u043b\u0435\u0436\u0430\u0442\u044c \u0434\u043e i3/i8, \u0432\u0438\u0433\u043e\u0442\u043e\u0432\u043b\u0435\u043d\u0438\u0445 \u0434\u043e 7/2014)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bmw_connected_drive/translations/zh-Hant.json b/homeassistant/components/bmw_connected_drive/translations/zh-Hant.json new file mode 100644 index 00000000000..fde5e1e3c94 --- /dev/null +++ b/homeassistant/components/bmw_connected_drive/translations/zh-Hant.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "region": "ConnectedDrive \u5340\u57df", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + }, + "options": { + "step": { + "account_options": { + "data": { + "read_only": "\u552f\u8b80\uff08\u50c5\u652f\u63f4\u50b3\u611f\u5668\u8207\u901a\u77e5\uff0c\u4e0d\n\u5305\u542b\u670d\u52d9\u8207\u9396\u5b9a\uff09", + "use_location": "\u4f7f\u7528 Home Assistant \u4f4d\u7f6e\u53d6\u5f97\u6c7d\u8eca\u4f4d\u7f6e\uff08\u9700\u8981\u70ba2014/7 \u524d\u751f\u7522\u7684\u975ei3/i8 \u8eca\u6b3e\uff09" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bond/translations/de.json b/homeassistant/components/bond/translations/de.json index 393232025dd..14f86a30bb2 100644 --- a/homeassistant/components/bond/translations/de.json +++ b/homeassistant/components/bond/translations/de.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindung nicht m\u00f6glich", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/bond/translations/tr.json b/homeassistant/components/bond/translations/tr.json new file mode 100644 index 00000000000..3488480a218 --- /dev/null +++ b/homeassistant/components/bond/translations/tr.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "confirm": { + "data": { + "access_token": "Eri\u015fim Belirteci" + } + }, + "user": { + "data": { + "access_token": "Eri\u015fim Belirteci", + "host": "Ana Bilgisayar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bond/translations/uk.json b/homeassistant/components/bond/translations/uk.json index d7da60ea178..95ede3d4329 100644 --- a/homeassistant/components/bond/translations/uk.json +++ b/homeassistant/components/bond/translations/uk.json @@ -1,7 +1,28 @@ { "config": { "abort": { - "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e" + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "old_firmware": "\u041d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u043e\u043d\u043e\u0432\u0438\u0442\u0438 \u043c\u0456\u043a\u0440\u043e\u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e. \u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u043d\u0430 \u0432\u0435\u0440\u0441\u0456\u044f \u0437\u0430\u0441\u0442\u0430\u0440\u0456\u043b\u0430 \u0456 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0454\u044e.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "flow_title": "Bond {bond_id} ({host})", + "step": { + "confirm": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443" + }, + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {bond_id}?" + }, + "user": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443", + "host": "\u0425\u043e\u0441\u0442" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/de.json b/homeassistant/components/braviatv/translations/de.json index b17d42ffaed..8ac8c09e4fe 100644 --- a/homeassistant/components/braviatv/translations/de.json +++ b/homeassistant/components/braviatv/translations/de.json @@ -1,15 +1,18 @@ { "config": { "abort": { - "already_configured": "Dieser Fernseher ist bereits konfiguriert." + "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, ung\u00fcltiger Host- oder PIN-Code.", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse", "unsupported_model": "Ihr TV-Modell wird nicht unterst\u00fctzt." }, "step": { "authorize": { + "data": { + "pin": "PIN-Code" + }, "description": "Geben Sie den auf dem Sony Bravia-Fernseher angezeigten PIN-Code ein. \n\nWenn der PIN-Code nicht angezeigt wird, m\u00fcssen Sie die Registrierung von Home Assistant auf Ihrem Fernseher aufheben, gehen Sie daf\u00fcr zu: Einstellungen -> Netzwerk -> Remote - Ger\u00e4teeinstellungen -> Registrierung des entfernten Ger\u00e4ts aufheben.", "title": "Autorisieren Sie Sony Bravia TV" }, diff --git a/homeassistant/components/braviatv/translations/tr.json b/homeassistant/components/braviatv/translations/tr.json new file mode 100644 index 00000000000..0853c8028fc --- /dev/null +++ b/homeassistant/components/braviatv/translations/tr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unsupported_model": "TV modeliniz desteklenmiyor." + }, + "step": { + "authorize": { + "title": "Sony Bravia TV'yi yetkilendirin" + }, + "user": { + "data": { + "host": "Ana Bilgisayar" + }, + "title": "Sony Bravia TV" + } + } + }, + "options": { + "step": { + "user": { + "title": "Sony Bravia TV i\u00e7in se\u00e7enekler" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/braviatv/translations/uk.json b/homeassistant/components/braviatv/translations/uk.json new file mode 100644 index 00000000000..7f66329c57e --- /dev/null +++ b/homeassistant/components/braviatv/translations/uk.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "no_ip_control": "\u041d\u0430 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0456 \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a\u0435\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u043e IP, \u0430\u0431\u043e \u0446\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_host": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430.", + "unsupported_model": "\u0426\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f." + }, + "step": { + "authorize": { + "data": { + "pin": "PIN-\u043a\u043e\u0434" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c PIN-\u043a\u043e\u0434, \u044f\u043a\u0438\u0439 \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u0454\u0442\u044c\u0441\u044f \u043d\u0430 \u0435\u043a\u0440\u0430\u043d\u0456 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 Sony Bravia. \n\n\u042f\u043a\u0449\u043e \u0412\u0438 \u043d\u0435 \u0431\u0430\u0447\u0438\u0442\u0435 PIN-\u043a\u043e\u0434, \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u0441\u043a\u0430\u0441\u0443\u0432\u0430\u0442\u0438 \u0440\u0435\u0454\u0441\u0442\u0440\u0430\u0446\u0456\u044e Home Assistant \u043d\u0430 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0456. \u041f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0432 \u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 - > \u041c\u0435\u0440\u0435\u0436\u0430 - > \u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0456\u0434\u0434\u0430\u043b\u0435\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e - > \u0421\u043a\u0430\u0441\u0443\u0432\u0430\u0442\u0438 \u0440\u0435\u0454\u0441\u0442\u0440\u0430\u0446\u0456\u044e \u0432\u0456\u0434\u0434\u0430\u043b\u0435\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e.", + "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u044f \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 Sony Bravia" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0454\u044e \u043f\u043e \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457:\nhttps://www.home-assistant.io/integrations/braviatv", + "title": "\u0422\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 Sony Bravia" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "ignored_sources": "\u0421\u043f\u0438\u0441\u043e\u043a \u0456\u0433\u043d\u043e\u0440\u043e\u0432\u0430\u043d\u0438\u0445 \u0434\u0436\u0435\u0440\u0435\u043b" + }, + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 Sony Bravia" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/broadlink/device.py b/homeassistant/components/broadlink/device.py index 24f473c3262..be9c7626ac1 100644 --- a/homeassistant/components/broadlink/device.py +++ b/homeassistant/components/broadlink/device.py @@ -57,9 +57,7 @@ class BroadlinkDevice: Triggered when the device is renamed on the frontend. """ device_registry = await dr.async_get_registry(hass) - device_entry = device_registry.async_get_device( - {(DOMAIN, entry.unique_id)}, set() - ) + device_entry = device_registry.async_get_device({(DOMAIN, entry.unique_id)}) device_registry.async_update_device(device_entry.id, name=entry.title) await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/broadlink/remote.py b/homeassistant/components/broadlink/remote.py index 5d0d618d7be..116c97aeb31 100644 --- a/homeassistant/components/broadlink/remote.py +++ b/homeassistant/components/broadlink/remote.py @@ -23,7 +23,10 @@ from homeassistant.components.remote import ( ATTR_DEVICE, ATTR_NUM_REPEATS, DEFAULT_DELAY_SECS, + DOMAIN as RM_DOMAIN, PLATFORM_SCHEMA, + SERVICE_DELETE_COMMAND, + SUPPORT_DELETE_COMMAND, SUPPORT_LEARN_COMMAND, RemoteEntity, ) @@ -48,6 +51,8 @@ COMMAND_TYPES = [COMMAND_TYPE_IR, COMMAND_TYPE_RF] CODE_STORAGE_VERSION = 1 FLAG_STORAGE_VERSION = 1 + +CODE_SAVE_DELAY = 15 FLAG_SAVE_DELAY = 15 COMMAND_SCHEMA = vol.Schema( @@ -74,6 +79,10 @@ SERVICE_LEARN_SCHEMA = COMMAND_SCHEMA.extend( } ) +SERVICE_DELETE_SCHEMA = COMMAND_SCHEMA.extend( + {vol.Required(ATTR_DEVICE): vol.All(cv.string, vol.Length(min=1))} +) + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( {vol.Required(CONF_HOST): cv.string}, extra=vol.ALLOW_EXTRA ) @@ -149,7 +158,7 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity): @property def supported_features(self): """Flag supported features.""" - return SUPPORT_LEARN_COMMAND + return SUPPORT_LEARN_COMMAND | SUPPORT_DELETE_COMMAND @property def device_info(self): @@ -196,6 +205,11 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity): except ValueError as err: raise ValueError("Invalid code") from err + @callback + def get_codes(self): + """Return a dictionary of codes.""" + return self._codes + @callback def get_flags(self): """Return a dictionary of toggle flags. @@ -434,3 +448,52 @@ class BroadlinkRemote(RemoteEntity, RestoreEntity): self.hass.components.persistent_notification.async_dismiss( notification_id="learn_command" ) + + async def async_delete_command(self, **kwargs): + """Delete a list of commands from a remote.""" + kwargs = SERVICE_DELETE_SCHEMA(kwargs) + commands = kwargs[ATTR_COMMAND] + device = kwargs[ATTR_DEVICE] + service = f"{RM_DOMAIN}.{SERVICE_DELETE_COMMAND}" + + if not self._state: + _LOGGER.warning( + "%s canceled: %s entity is turned off", + service, + self.entity_id, + ) + return + + try: + codes = self._codes[device] + except KeyError as err: + err_msg = f"Device not found: {repr(device)}" + _LOGGER.error("Failed to call %s. %s", service, err_msg) + raise ValueError(err_msg) from err + + cmds_not_found = [] + for command in commands: + try: + del codes[command] + except KeyError: + cmds_not_found.append(command) + + if cmds_not_found: + if len(cmds_not_found) == 1: + err_msg = f"Command not found: {repr(cmds_not_found[0])}" + else: + err_msg = f"Commands not found: {repr(cmds_not_found)}" + + if len(cmds_not_found) == len(commands): + _LOGGER.error("Failed to call %s. %s", service, err_msg) + raise ValueError(err_msg) + + _LOGGER.error("Error during %s. %s", service, err_msg) + + # Clean up + if not codes: + del self._codes[device] + if self._flags.pop(device, None) is not None: + self._flag_storage.async_delay_save(self.get_flags, FLAG_SAVE_DELAY) + + self._code_storage.async_delay_save(self.get_codes, CODE_SAVE_DELAY) diff --git a/homeassistant/components/broadlink/translations/de.json b/homeassistant/components/broadlink/translations/de.json index f915040635f..5704efe37c6 100644 --- a/homeassistant/components/broadlink/translations/de.json +++ b/homeassistant/components/broadlink/translations/de.json @@ -1,13 +1,15 @@ { "config": { "abort": { - "cannot_connect": "Verbindungsfehler", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_host": "Ung\u00fcltiger Hostname oder IP Adresse", "not_supported": "Ger\u00e4t nicht unterst\u00fctzt", "unknown": "Unerwarteter Fehler" }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_host": "Ung\u00fcltiger Hostname oder IP Adresse", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/broadlink/translations/tr.json b/homeassistant/components/broadlink/translations/tr.json new file mode 100644 index 00000000000..d37a3203476 --- /dev/null +++ b/homeassistant/components/broadlink/translations/tr.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "not_supported": "Cihaz desteklenmiyor", + "unknown": "Beklenmeyen hata" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "auth": { + "title": "Cihaza kimlik do\u011frulama" + }, + "finish": { + "title": "Cihaz i\u00e7in bir isim se\u00e7in" + }, + "reset": { + "title": "Cihaz\u0131n kilidini a\u00e7\u0131n" + }, + "unlock": { + "data": { + "unlock": "Evet, yap." + }, + "title": "Cihaz\u0131n kilidini a\u00e7\u0131n (iste\u011fe ba\u011fl\u0131)" + }, + "user": { + "data": { + "host": "Ana Bilgisayar", + "timeout": "Zaman a\u015f\u0131m\u0131" + }, + "title": "Cihaza ba\u011flan\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/broadlink/translations/uk.json b/homeassistant/components/broadlink/translations/uk.json new file mode 100644 index 00000000000..ea3e3e75cd6 --- /dev/null +++ b/homeassistant/components/broadlink/translations/uk.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_host": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430.", + "not_supported": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_host": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "flow_title": "{name} ({model}, {host})", + "step": { + "auth": { + "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u044f \u043d\u0430 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457" + }, + "finish": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "title": "\u0412\u043a\u0430\u0436\u0456\u0442\u044c \u043d\u0430\u0437\u0432\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + }, + "reset": { + "description": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 {name} ({model}, {host}) \u0437\u0430\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e. \u0414\u043e\u0442\u0440\u0438\u043c\u0443\u0439\u0442\u0435\u0441\u044f \u0446\u0438\u0445 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0439, \u0449\u043e\u0431 \u0440\u043e\u0437\u0431\u043b\u043e\u043a\u0443\u0432\u0430\u0442\u0438 \u0439\u043e\u0433\u043e:\n 1. \u0412\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0443 Broadlink.\n 2. \u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439.\n 3. \u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c `...` \u0432 \u043f\u0440\u0430\u0432\u043e\u043c\u0443 \u0432\u0435\u0440\u0445\u043d\u044c\u043e\u043c\u0443 \u043a\u0443\u0442\u0456.\n 4. \u041f\u0440\u043e\u043a\u0440\u0443\u0442\u0456\u0442\u044c \u0441\u0442\u043e\u0440\u0456\u043d\u043a\u0443 \u0432\u043d\u0438\u0437.\n 5. \u0412\u0438\u043c\u043a\u043d\u0456\u0442\u044c \u0431\u043b\u043e\u043a\u0443\u0432\u0430\u043d\u043d\u044f.", + "title": "\u0420\u043e\u0437\u0431\u043b\u043e\u043a\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + }, + "unlock": { + "data": { + "unlock": "\u0422\u0430\u043a, \u0437\u0440\u043e\u0431\u0438\u0442\u0438 \u0446\u0435." + }, + "description": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 {name} ({model}, {host}) \u0437\u0430\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e. \u0426\u0435 \u043c\u043e\u0436\u0435 \u043f\u0440\u0438\u0432\u0435\u0441\u0442\u0438 \u0434\u043e \u043f\u0440\u043e\u0431\u043b\u0435\u043c \u0437 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0454\u044e \u0432 Home Assistant. \u0425\u043e\u0447\u0435\u0442\u0435 \u0439\u043e\u0433\u043e \u0440\u043e\u0437\u0431\u043b\u043e\u043a\u0443\u0432\u0430\u0442\u0438?", + "title": "\u0420\u043e\u0437\u0431\u043b\u043e\u043a\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442" + }, + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/brother/translations/de.json b/homeassistant/components/brother/translations/de.json index 72bd052cc1d..c2a7ae8ec76 100644 --- a/homeassistant/components/brother/translations/de.json +++ b/homeassistant/components/brother/translations/de.json @@ -5,7 +5,7 @@ "unsupported_model": "Dieses Druckermodell wird nicht unterst\u00fctzt." }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", "snmp_error": "SNMP-Server deaktiviert oder Drucker nicht unterst\u00fctzt.", "wrong_host": " Ung\u00fcltiger Hostname oder IP-Adresse" }, diff --git a/homeassistant/components/brother/translations/tr.json b/homeassistant/components/brother/translations/tr.json index 160a5ecc7b7..cd91a485252 100644 --- a/homeassistant/components/brother/translations/tr.json +++ b/homeassistant/components/brother/translations/tr.json @@ -1,6 +1,20 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "unsupported_model": "Bu yaz\u0131c\u0131 modeli desteklenmiyor." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "flow_title": "Brother Yaz\u0131c\u0131: {model} {serial_number}", "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "type": "Yaz\u0131c\u0131n\u0131n t\u00fcr\u00fc" + } + }, "zeroconf_confirm": { "title": "Ke\u015ffedilen Brother Yaz\u0131c\u0131" } diff --git a/homeassistant/components/brother/translations/uk.json b/homeassistant/components/brother/translations/uk.json new file mode 100644 index 00000000000..ac5943aa85c --- /dev/null +++ b/homeassistant/components/brother/translations/uk.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "unsupported_model": "\u0426\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "snmp_error": "\u0421\u0435\u0440\u0432\u0435\u0440 SNMP \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u0438\u0439 \u0430\u0431\u043e \u043f\u0440\u0438\u043d\u0442\u0435\u0440 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f.", + "wrong_host": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430." + }, + "flow_title": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440 Brother: {model} {serial_number}", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "type": "\u0422\u0438\u043f \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430" + }, + "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0454\u044e \u043f\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044e \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457: https://www.home-assistant.io/integrations/brother." + }, + "zeroconf_confirm": { + "data": { + "type": "\u0422\u0438\u043f \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430" + }, + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 \u043f\u0440\u0438\u043d\u0442\u0435\u0440 Brother {model} \u0437 \u0441\u0435\u0440\u0456\u0439\u043d\u0438\u043c \u043d\u043e\u043c\u0435\u0440\u043e\u043c `{serial_number}`?", + "title": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u043f\u0440\u0438\u043d\u0442\u0435\u0440 Brother" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/bsblan/translations/de.json b/homeassistant/components/bsblan/translations/de.json index 5fd61c0bfed..971e3c1ea8a 100644 --- a/homeassistant/components/bsblan/translations/de.json +++ b/homeassistant/components/bsblan/translations/de.json @@ -4,7 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "user": { diff --git a/homeassistant/components/bsblan/translations/tr.json b/homeassistant/components/bsblan/translations/tr.json index 94acde2d0a3..803b5102a07 100644 --- a/homeassistant/components/bsblan/translations/tr.json +++ b/homeassistant/components/bsblan/translations/tr.json @@ -1,9 +1,17 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, "step": { "user": { "data": { + "host": "Ana Bilgisayar", "password": "\u015eifre", + "port": "Port", "username": "Kullan\u0131c\u0131 ad\u0131" } } diff --git a/homeassistant/components/bsblan/translations/uk.json b/homeassistant/components/bsblan/translations/uk.json new file mode 100644 index 00000000000..619f7c8e8a5 --- /dev/null +++ b/homeassistant/components/bsblan/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "flow_title": "BSB-Lan: {name}", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "passkey": "\u041f\u0430\u0440\u043e\u043b\u044c", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 BSB-Lan.", + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/caldav/manifest.json b/homeassistant/components/caldav/manifest.json index 94d786c8825..992b79f0d3b 100644 --- a/homeassistant/components/caldav/manifest.json +++ b/homeassistant/components/caldav/manifest.json @@ -2,6 +2,6 @@ "domain": "caldav", "name": "CalDAV", "documentation": "https://www.home-assistant.io/integrations/caldav", - "requirements": ["caldav==0.6.1"], + "requirements": ["caldav==0.7.1"], "codeowners": [] } diff --git a/homeassistant/components/canary/translations/de.json b/homeassistant/components/canary/translations/de.json index eebc9bd5fc3..bdd746c3149 100644 --- a/homeassistant/components/canary/translations/de.json +++ b/homeassistant/components/canary/translations/de.json @@ -1,10 +1,11 @@ { "config": { "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", "unknown": "Unerwarteter Fehler" }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "flow_title": "Canary: {name}", "step": { diff --git a/homeassistant/components/canary/translations/tr.json b/homeassistant/components/canary/translations/tr.json new file mode 100644 index 00000000000..6d18629b067 --- /dev/null +++ b/homeassistant/components/canary/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "unknown": "Beklenmeyen hata" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/canary/translations/uk.json b/homeassistant/components/canary/translations/uk.json new file mode 100644 index 00000000000..74327f3ebd6 --- /dev/null +++ b/homeassistant/components/canary/translations/uk.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "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.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "flow_title": "Canary: {name}", + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e Canary" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ffmpeg_arguments": "\u0410\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u0438, \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u0456 \u0432 ffmpeg \u0434\u043b\u044f \u043a\u0430\u043c\u0435\u0440", + "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 \u0437\u0430\u043f\u0438\u0442\u0443 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/discovery.py b/homeassistant/components/cast/discovery.py index 341ba0c4c5e..4858d37f732 100644 --- a/homeassistant/components/cast/discovery.py +++ b/homeassistant/components/cast/discovery.py @@ -25,6 +25,7 @@ def discover_chromecast(hass: HomeAssistant, info: ChromecastInfo): _LOGGER.error("Discovered chromecast without uuid %s", info) return + info = info.fill_out_missing_chromecast_info() if info.uuid in hass.data[KNOWN_CHROMECAST_INFO_KEY]: _LOGGER.debug("Discovered update for known chromecast %s", info) else: diff --git a/homeassistant/components/cast/helpers.py b/homeassistant/components/cast/helpers.py index 0ad13d137d1..e7db380406b 100644 --- a/homeassistant/components/cast/helpers.py +++ b/homeassistant/components/cast/helpers.py @@ -1,7 +1,8 @@ """Helpers to deal with Cast devices.""" -from typing import Optional, Tuple +from typing import Optional import attr +from pychromecast import dial from pychromecast.const import CAST_MANUFACTURERS from .const import DEFAULT_PORT @@ -20,8 +21,10 @@ class ChromecastInfo: uuid: Optional[str] = attr.ib( converter=attr.converters.optional(str), default=None ) # always convert UUID to string if not None + _manufacturer = attr.ib(type=Optional[str], default=None) model_name: str = attr.ib(default="") friendly_name: Optional[str] = attr.ib(default=None) + is_dynamic_group = attr.ib(type=Optional[bool], default=None) @property def is_audio_group(self) -> bool: @@ -29,17 +32,84 @@ class ChromecastInfo: return self.port != DEFAULT_PORT @property - def host_port(self) -> Tuple[str, int]: - """Return the host+port tuple.""" - return self.host, self.port + def is_information_complete(self) -> bool: + """Return if all information is filled out.""" + want_dynamic_group = self.is_audio_group + have_dynamic_group = self.is_dynamic_group is not None + have_all_except_dynamic_group = all( + attr.astuple( + self, + filter=attr.filters.exclude( + attr.fields(ChromecastInfo).is_dynamic_group + ), + ) + ) + return have_all_except_dynamic_group and ( + not want_dynamic_group or have_dynamic_group + ) @property def manufacturer(self) -> str: """Return the manufacturer.""" + if self._manufacturer: + return self._manufacturer if not self.model_name: return None return CAST_MANUFACTURERS.get(self.model_name.lower(), "Google Inc.") + def fill_out_missing_chromecast_info(self) -> "ChromecastInfo": + """Return a new ChromecastInfo object with missing attributes filled in. + + Uses blocking HTTP / HTTPS. + """ + if self.is_information_complete: + # We have all information, no need to check HTTP API. + return self + + # Fill out missing group information via HTTP API. + if self.is_audio_group: + is_dynamic_group = False + http_group_status = None + if self.uuid: + http_group_status = dial.get_multizone_status( + self.host, + services=self.services, + zconf=ChromeCastZeroconf.get_zeroconf(), + ) + if http_group_status is not None: + is_dynamic_group = any( + str(g.uuid) == self.uuid + for g in http_group_status.dynamic_groups + ) + + return ChromecastInfo( + services=self.services, + host=self.host, + port=self.port, + uuid=self.uuid, + friendly_name=self.friendly_name, + model_name=self.model_name, + is_dynamic_group=is_dynamic_group, + ) + + # Fill out some missing information (friendly_name, uuid) via HTTP dial. + http_device_status = dial.get_device_status( + self.host, services=self.services, zconf=ChromeCastZeroconf.get_zeroconf() + ) + if http_device_status is None: + # HTTP dial didn't give us any new information. + return self + + return ChromecastInfo( + services=self.services, + host=self.host, + port=self.port, + uuid=(self.uuid or http_device_status.uuid), + friendly_name=(self.friendly_name or http_device_status.friendly_name), + manufacturer=(self.manufacturer or http_device_status.manufacturer), + model_name=(self.model_name or http_device_status.model_name), + ) + class ChromeCastZeroconf: """Class to hold a zeroconf instance.""" @@ -65,19 +135,22 @@ class CastStatusListener: potentially arrive. This class allows invalidating past chromecast objects. """ - def __init__(self, cast_device, chromecast, mz_mgr): + def __init__(self, cast_device, chromecast, mz_mgr, mz_only=False): """Initialize the status listener.""" self._cast_device = cast_device self._uuid = chromecast.uuid self._valid = True self._mz_mgr = mz_mgr + if cast_device._cast_info.is_audio_group: + self._mz_mgr.add_multizone(chromecast) + if mz_only: + return + chromecast.register_status_listener(self) chromecast.socket_client.media_controller.register_status_listener(self) chromecast.register_connection_listener(self) - if cast_device._cast_info.is_audio_group: - self._mz_mgr.add_multizone(chromecast) - else: + if not cast_device._cast_info.is_audio_group: self._mz_mgr.register_listener(chromecast.uuid, self) def new_cast_status(self, cast_status): diff --git a/homeassistant/components/cast/manifest.json b/homeassistant/components/cast/manifest.json index 8072e06c2e5..88dabc8d04d 100644 --- a/homeassistant/components/cast/manifest.json +++ b/homeassistant/components/cast/manifest.json @@ -3,7 +3,7 @@ "name": "Google Cast", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/cast", - "requirements": ["pychromecast==7.6.0"], + "requirements": ["pychromecast==8.0.0"], "after_dependencies": ["cloud", "http", "media_source", "plex", "tts", "zeroconf"], "zeroconf": ["_googlecast._tcp.local."], "codeowners": ["@emontnemery"] diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index e68800efb44..6bedae1cac5 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -63,6 +63,7 @@ from .const import ( DOMAIN as CAST_DOMAIN, KNOWN_CHROMECAST_INFO_KEY, SIGNAL_CAST_DISCOVERED, + SIGNAL_CAST_REMOVED, SIGNAL_HASS_CAST_SHOW_VIEW, ) from .discovery import setup_internal_discovery @@ -115,6 +116,13 @@ def _async_create_cast_device(hass: HomeAssistantType, info: ChromecastInfo): return None # -> New cast device added_casts.add(info.uuid) + + if info.is_dynamic_group: + # This is a dynamic group, do not add it but connect to the service. + group = DynamicCastGroup(hass, info) + group.async_setup() + return None + return CastDevice(info) @@ -206,8 +214,9 @@ class CastDevice(MediaPlayerEntity): self.hass, SIGNAL_CAST_DISCOVERED, self._async_cast_discovered ) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_stop) + self.async_set_cast_info(self._cast_info) self.hass.async_create_task( - async_create_catching_coro(self.async_set_cast_info(self._cast_info)) + async_create_catching_coro(self.async_connect_to_chromecast()) ) self._cast_view_remove_handler = async_dispatcher_connect( @@ -228,15 +237,13 @@ class CastDevice(MediaPlayerEntity): self._cast_view_remove_handler() self._cast_view_remove_handler = None - async def async_set_cast_info(self, cast_info): - """Set the cast information and set up the chromecast object.""" + def async_set_cast_info(self, cast_info): + """Set the cast information.""" self._cast_info = cast_info - if self._chromecast is not None: - # Only setup the chromecast once, added elements to services - # will automatically be picked up. - return + async def async_connect_to_chromecast(self): + """Set up the chromecast object.""" _LOGGER.debug( "[%s %s] Connecting to cast device by service %s", @@ -248,9 +255,9 @@ class CastDevice(MediaPlayerEntity): pychromecast.get_chromecast_from_service, ( self.services, - cast_info.uuid, - cast_info.model_name, - cast_info.friendly_name, + self._cast_info.uuid, + self._cast_info.model_name, + self._cast_info.friendly_name, None, None, ), @@ -777,16 +784,12 @@ class CastDevice(MediaPlayerEntity): async def _async_cast_discovered(self, discover: ChromecastInfo): """Handle discovery of new Chromecast.""" - if self._cast_info.uuid is None: - # We can't handle empty UUIDs - return - if self._cast_info.uuid != discover.uuid: # Discovered is not our device. return _LOGGER.debug("Discovered chromecast with same UUID: %s", discover) - await self.async_set_cast_info(discover) + self.async_set_cast_info(discover) async def _async_stop(self, event): """Disconnect socket on Home Assistant stop.""" @@ -808,3 +811,131 @@ class CastDevice(MediaPlayerEntity): self._chromecast.register_handler(controller) self._hass_cast_controller.show_lovelace_view(view_path, url_path) + + +class DynamicCastGroup: + """Representation of a Cast device on the network - for dynamic cast groups.""" + + def __init__(self, hass, cast_info: ChromecastInfo): + """Initialize the cast device.""" + + self.hass = hass + self._cast_info = cast_info + self.services = cast_info.services + self._chromecast: Optional[pychromecast.Chromecast] = None + self.mz_mgr = None + self._status_listener: Optional[CastStatusListener] = None + + self._add_remove_handler = None + self._del_remove_handler = None + + def async_setup(self): + """Create chromecast object.""" + self._add_remove_handler = async_dispatcher_connect( + self.hass, SIGNAL_CAST_DISCOVERED, self._async_cast_discovered + ) + self._del_remove_handler = async_dispatcher_connect( + self.hass, SIGNAL_CAST_REMOVED, self._async_cast_removed + ) + self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self._async_stop) + self.async_set_cast_info(self._cast_info) + self.hass.async_create_task( + async_create_catching_coro(self.async_connect_to_chromecast()) + ) + + async def async_tear_down(self) -> None: + """Disconnect Chromecast object.""" + await self._async_disconnect() + if self._cast_info.uuid is not None: + # Remove the entity from the added casts so that it can dynamically + # be re-added again. + self.hass.data[ADDED_CAST_DEVICES_KEY].remove(self._cast_info.uuid) + if self._add_remove_handler: + self._add_remove_handler() + self._add_remove_handler = None + if self._del_remove_handler: + self._del_remove_handler() + self._del_remove_handler = None + + def async_set_cast_info(self, cast_info): + """Set the cast information and set up the chromecast object.""" + + self._cast_info = cast_info + + async def async_connect_to_chromecast(self): + """Set the cast information and set up the chromecast object.""" + + _LOGGER.debug( + "[%s %s] Connecting to cast device by service %s", + "Dynamic group", + self._cast_info.friendly_name, + self.services, + ) + chromecast = await self.hass.async_add_executor_job( + pychromecast.get_chromecast_from_service, + ( + self.services, + self._cast_info.uuid, + self._cast_info.model_name, + self._cast_info.friendly_name, + None, + None, + ), + ChromeCastZeroconf.get_zeroconf(), + ) + self._chromecast = chromecast + + if CAST_MULTIZONE_MANAGER_KEY not in self.hass.data: + self.hass.data[CAST_MULTIZONE_MANAGER_KEY] = MultizoneManager() + + self.mz_mgr = self.hass.data[CAST_MULTIZONE_MANAGER_KEY] + + self._status_listener = CastStatusListener(self, chromecast, self.mz_mgr, True) + self._chromecast.start() + + async def _async_disconnect(self): + """Disconnect Chromecast object if it is set.""" + if self._chromecast is None: + # Can't disconnect if not connected. + return + _LOGGER.debug( + "[%s %s] Disconnecting from chromecast socket", + "Dynamic group", + self._cast_info.friendly_name, + ) + + await self.hass.async_add_executor_job(self._chromecast.disconnect) + + self._invalidate() + + def _invalidate(self): + """Invalidate some attributes.""" + self._chromecast = None + self.mz_mgr = None + if self._status_listener is not None: + self._status_listener.invalidate() + self._status_listener = None + + async def _async_cast_discovered(self, discover: ChromecastInfo): + """Handle discovery of new Chromecast.""" + if self._cast_info.uuid != discover.uuid: + # Discovered is not our device. + return + + _LOGGER.debug("Discovered dynamic group with same UUID: %s", discover) + self.async_set_cast_info(discover) + + async def _async_cast_removed(self, discover: ChromecastInfo): + """Handle removal of Chromecast.""" + if self._cast_info.uuid != discover.uuid: + # Removed is not our device. + return + + if not discover.services: + # Clean up the dynamic group + _LOGGER.debug("Clean up dynamic group: %s", discover) + await self.async_tear_down() + + async def _async_stop(self, event): + """Disconnect socket on Home Assistant stop.""" + await self._async_disconnect() diff --git a/homeassistant/components/cast/translations/de.json b/homeassistant/components/cast/translations/de.json index 87f8e7cb2bc..7ff1efb8ee0 100644 --- a/homeassistant/components/cast/translations/de.json +++ b/homeassistant/components/cast/translations/de.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "Keine Google Cast Ger\u00e4te im Netzwerk gefunden.", - "single_instance_allowed": "Nur eine einzige Konfiguration von Google Cast ist notwendig." + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden.", + "single_instance_allowed": "Bereits konfiguriert. Es ist nur eine Konfiguration m\u00f6glich." }, "step": { "confirm": { diff --git a/homeassistant/components/cast/translations/tr.json b/homeassistant/components/cast/translations/tr.json new file mode 100644 index 00000000000..8de4663957e --- /dev/null +++ b/homeassistant/components/cast/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "confirm": { + "description": "Kuruluma ba\u015flamak ister misiniz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cast/translations/uk.json b/homeassistant/components/cast/translations/uk.json index 783defdca25..292861e9129 100644 --- a/homeassistant/components/cast/translations/uk.json +++ b/homeassistant/components/cast/translations/uk.json @@ -1,8 +1,12 @@ { "config": { + "abort": { + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "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." + }, "step": { "confirm": { - "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Google Cast?" + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?" } } } diff --git a/homeassistant/components/cert_expiry/translations/tr.json b/homeassistant/components/cert_expiry/translations/tr.json new file mode 100644 index 00000000000..6c05bef3a65 --- /dev/null +++ b/homeassistant/components/cert_expiry/translations/tr.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cert_expiry/translations/uk.json b/homeassistant/components/cert_expiry/translations/uk.json new file mode 100644 index 00000000000..997e12a8cb2 --- /dev/null +++ b/homeassistant/components/cert_expiry/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant.", + "import_failed": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0456\u043c\u043f\u043e\u0440\u0442\u0443 \u0437 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457." + }, + "error": { + "connection_refused": "\u041f\u0440\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u0456 \u0434\u043e \u0445\u043e\u0441\u0442\u0443 \u0431\u0443\u043b\u043e \u0432\u0456\u0434\u043c\u043e\u0432\u043b\u0435\u043d\u043e \u0432 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u0456.", + "connection_timeout": "\u0427\u0430\u0441 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u0445\u043e\u0441\u0442\u0430 \u043c\u0438\u043d\u0443\u0432.", + "resolve_failed": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044c \u0434\u043e \u0445\u043e\u0441\u0442\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "\u0422\u0435\u0440\u043c\u0456\u043d \u0434\u0456\u0457 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430" + } + } + }, + "title": "\u0422\u0435\u0440\u043c\u0456\u043d \u0434\u0456\u0457 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430" +} \ No newline at end of file diff --git a/homeassistant/components/climate/translations/tr.json b/homeassistant/components/climate/translations/tr.json index 0b027dbd87f..201fec4c4b6 100644 --- a/homeassistant/components/climate/translations/tr.json +++ b/homeassistant/components/climate/translations/tr.json @@ -1,4 +1,10 @@ { + "device_automation": { + "action_type": { + "set_hvac_mode": "{entity_name} \u00fczerinde HVAC modunu de\u011fi\u015ftir", + "set_preset_mode": "{entity_name} \u00fczerindeki \u00f6n ayar\u0131 de\u011fi\u015ftir" + } + }, "state": { "_": { "auto": "Otomatik", diff --git a/homeassistant/components/climate/translations/uk.json b/homeassistant/components/climate/translations/uk.json index 8d636c386e5..de6baff021c 100644 --- a/homeassistant/components/climate/translations/uk.json +++ b/homeassistant/components/climate/translations/uk.json @@ -1,17 +1,17 @@ { "device_automation": { "action_type": { - "set_hvac_mode": "\u0417\u043c\u0456\u043d\u0438\u0442\u0438 \u0440\u0435\u0436\u0438\u043c HVAC \u043d\u0430 {entity_name}", - "set_preset_mode": "\u0417\u043c\u0456\u043d\u0438\u0442\u0438 \u043f\u043e\u043f\u0435\u0440\u0435\u0434\u043d\u044c\u043e \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043d\u0430 {entity_name}" + "set_hvac_mode": "{entity_name}: \u0437\u043c\u0456\u043d\u0438\u0442\u0438 \u0440\u0435\u0436\u0438\u043c \u0440\u043e\u0431\u043e\u0442\u0438", + "set_preset_mode": "{entity_name}: \u0437\u043c\u0456\u043d\u0438\u0442\u0438 \u043f\u0440\u0435\u0441\u0435\u0442" }, "condition_type": { - "is_hvac_mode": "{entity_name} \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e \u0432 \u043f\u0435\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c HVAC", + "is_hvac_mode": "{entity_name} \u0437\u043d\u0430\u0445\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0432 \u0437\u0430\u0434\u0430\u043d\u043e\u043c\u0443 \u0440\u0435\u0436\u0438\u043c\u0456 \u0440\u043e\u0431\u043e\u0442\u0438", "is_preset_mode": "{entity_name} \u043d\u0430\u0441\u0442\u0440\u043e\u0454\u043d\u043e \u043d\u0430 \u043f\u0435\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c" }, "trigger_type": { - "current_humidity_changed": "{entity_name} \u0432\u0438\u043c\u0456\u0440\u044f\u043d\u0430 \u0432\u043e\u043b\u043e\u0433\u0456\u0441\u0442\u044c \u0437\u043c\u0456\u043d\u0435\u043d\u0430", - "current_temperature_changed": "{entity_name} \u0432\u0438\u043c\u0456\u0440\u044f\u043d\u0443 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0443 \u0437\u043c\u0456\u043d\u0435\u043d\u043e", - "hvac_mode_changed": "{entity_name} \u0420\u0435\u0436\u0438\u043c HVAC \u0437\u043c\u0456\u043d\u0435\u043d\u043e" + "current_humidity_changed": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0432\u0438\u043c\u0456\u0440\u044f\u043d\u043e\u0457 \u0432\u043e\u043b\u043e\u0433\u043e\u0441\u0442\u0456", + "current_temperature_changed": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0432\u0438\u043c\u0456\u0440\u044f\u043d\u043e\u0457 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0438", + "hvac_mode_changed": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0440\u0435\u0436\u0438\u043c \u0440\u043e\u0431\u043e\u0442\u0438" } }, "state": { @@ -21,7 +21,7 @@ "dry": "\u041e\u0441\u0443\u0448\u0435\u043d\u043d\u044f", "fan_only": "\u041b\u0438\u0448\u0435 \u0432\u0435\u043d\u0442\u0438\u043b\u044f\u0442\u043e\u0440", "heat": "\u041d\u0430\u0433\u0440\u0456\u0432\u0430\u043d\u043d\u044f", - "heat_cool": "\u041d\u0430\u0433\u0440\u0456\u0432\u0430\u043d\u043d\u044f/\u041e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f", + "heat_cool": "\u041d\u0430\u0433\u0440\u0456\u0432\u0430\u043d\u043d\u044f / \u041e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f", "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e" } }, diff --git a/homeassistant/components/cloud/const.py b/homeassistant/components/cloud/const.py index 3c7804970fb..d0417e0d38d 100644 --- a/homeassistant/components/cloud/const.py +++ b/homeassistant/components/cloud/const.py @@ -20,6 +20,8 @@ PREF_GOOGLE_LOCAL_WEBHOOK_ID = "google_local_webhook_id" PREF_USERNAME = "username" PREF_ALEXA_DEFAULT_EXPOSE = "alexa_default_expose" PREF_GOOGLE_DEFAULT_EXPOSE = "google_default_expose" +PREF_TTS_DEFAULT_VOICE = "tts_default_voice" +DEFAULT_TTS_DEFAULT_VOICE = ("en-US", "female") DEFAULT_DISABLE_2FA = False DEFAULT_ALEXA_REPORT_STATE = False DEFAULT_GOOGLE_REPORT_STATE = False diff --git a/homeassistant/components/cloud/http_api.py b/homeassistant/components/cloud/http_api.py index a4d8b84b1ad..2bcc37fec05 100644 --- a/homeassistant/components/cloud/http_api.py +++ b/homeassistant/components/cloud/http_api.py @@ -8,6 +8,7 @@ import async_timeout import attr from hass_nabucasa import Cloud, auth, thingtalk from hass_nabucasa.const import STATE_DISCONNECTED +from hass_nabucasa.voice import MAP_VOICE import voluptuous as vol from homeassistant.components import websocket_api @@ -37,6 +38,7 @@ from .const import ( PREF_GOOGLE_DEFAULT_EXPOSE, PREF_GOOGLE_REPORT_STATE, PREF_GOOGLE_SECURE_DEVICES_PIN, + PREF_TTS_DEFAULT_VOICE, REQUEST_TIMEOUT, InvalidTrustedNetworks, InvalidTrustedProxies, @@ -115,6 +117,7 @@ async def async_setup(hass): async_register_command(alexa_sync) async_register_command(thingtalk_convert) + async_register_command(tts_info) hass.http.register_view(GoogleActionsSyncView) hass.http.register_view(CloudLoginView) @@ -385,6 +388,9 @@ async def websocket_subscription(hass, connection, msg): vol.Optional(PREF_ALEXA_DEFAULT_EXPOSE): [str], vol.Optional(PREF_GOOGLE_DEFAULT_EXPOSE): [str], vol.Optional(PREF_GOOGLE_SECURE_DEVICES_PIN): vol.Any(None, str), + vol.Optional(PREF_TTS_DEFAULT_VOICE): vol.All( + vol.Coerce(tuple), vol.In(MAP_VOICE) + ), } ) async def websocket_update_prefs(hass, connection, msg): @@ -637,3 +643,11 @@ async def thingtalk_convert(hass, connection, msg): ) except thingtalk.ThingTalkConversionError as err: connection.send_error(msg["id"], ws_const.ERR_UNKNOWN_ERROR, str(err)) + + +@websocket_api.websocket_command({"type": "cloud/tts/info"}) +def tts_info(hass, connection, msg): + """Fetch available tts info.""" + connection.send_result( + msg["id"], {"languages": [(lang, gender.value) for lang, gender in MAP_VOICE]} + ) diff --git a/homeassistant/components/cloud/manifest.json b/homeassistant/components/cloud/manifest.json index 03bf2761857..9d27de13309 100644 --- a/homeassistant/components/cloud/manifest.json +++ b/homeassistant/components/cloud/manifest.json @@ -2,7 +2,7 @@ "domain": "cloud", "name": "Home Assistant Cloud", "documentation": "https://www.home-assistant.io/integrations/cloud", - "requirements": ["hass-nabucasa==0.39.0"], + "requirements": ["hass-nabucasa==0.41.0"], "dependencies": ["http", "webhook", "alexa"], "after_dependencies": ["google_assistant"], "codeowners": ["@home-assistant/cloud"] diff --git a/homeassistant/components/cloud/prefs.py b/homeassistant/components/cloud/prefs.py index 6e0e78839c1..a15eafc4d08 100644 --- a/homeassistant/components/cloud/prefs.py +++ b/homeassistant/components/cloud/prefs.py @@ -12,6 +12,7 @@ from .const import ( DEFAULT_ALEXA_REPORT_STATE, DEFAULT_EXPOSED_DOMAINS, DEFAULT_GOOGLE_REPORT_STATE, + DEFAULT_TTS_DEFAULT_VOICE, DOMAIN, PREF_ALEXA_DEFAULT_EXPOSE, PREF_ALEXA_ENTITY_CONFIGS, @@ -30,6 +31,7 @@ from .const import ( PREF_GOOGLE_SECURE_DEVICES_PIN, PREF_OVERRIDE_NAME, PREF_SHOULD_EXPOSE, + PREF_TTS_DEFAULT_VOICE, PREF_USERNAME, InvalidTrustedNetworks, InvalidTrustedProxies, @@ -86,6 +88,7 @@ class CloudPreferences: google_report_state=UNDEFINED, alexa_default_expose=UNDEFINED, google_default_expose=UNDEFINED, + tts_default_voice=UNDEFINED, ): """Update user preferences.""" prefs = {**self._prefs} @@ -103,6 +106,7 @@ class CloudPreferences: (PREF_GOOGLE_REPORT_STATE, google_report_state), (PREF_ALEXA_DEFAULT_EXPOSE, alexa_default_expose), (PREF_GOOGLE_DEFAULT_EXPOSE, google_default_expose), + (PREF_TTS_DEFAULT_VOICE, tts_default_voice), ): if value is not UNDEFINED: prefs[key] = value @@ -203,6 +207,7 @@ class CloudPreferences: PREF_GOOGLE_ENTITY_CONFIGS: self.google_entity_configs, PREF_GOOGLE_REPORT_STATE: self.google_report_state, PREF_GOOGLE_SECURE_DEVICES_PIN: self.google_secure_devices_pin, + PREF_TTS_DEFAULT_VOICE: self.tts_default_voice, } @property @@ -279,6 +284,11 @@ class CloudPreferences: """Return the published cloud webhooks.""" return self._prefs.get(PREF_CLOUDHOOKS, {}) + @property + def tts_default_voice(self): + """Return the default TTS voice.""" + return self._prefs.get(PREF_TTS_DEFAULT_VOICE, DEFAULT_TTS_DEFAULT_VOICE) + async def get_cloud_user(self) -> str: """Return ID from Home Assistant Cloud system user.""" user = await self._load_cloud_user() diff --git a/homeassistant/components/cloud/translations/ca.json b/homeassistant/components/cloud/translations/ca.json index fede749c7dd..4e6a14cd2f0 100644 --- a/homeassistant/components/cloud/translations/ca.json +++ b/homeassistant/components/cloud/translations/ca.json @@ -2,9 +2,9 @@ "system_health": { "info": { "alexa_enabled": "Alexa activada", - "can_reach_cert_server": "Servidor de certificaci\u00f3 accessible", - "can_reach_cloud": "Home Assistant Cloud accessible", - "can_reach_cloud_auth": "Servidor d'autenticaci\u00f3 accessible", + "can_reach_cert_server": "Acc\u00e9s al servidor de certificaci\u00f3", + "can_reach_cloud": "Acc\u00e9s a Home Assistant Cloud", + "can_reach_cloud_auth": "Acc\u00e9s al servidor d'autenticaci\u00f3", "google_enabled": "Google activat", "logged_in": "Sessi\u00f3 iniciada", "relayer_connected": "Encaminador connectat", diff --git a/homeassistant/components/cloud/translations/de.json b/homeassistant/components/cloud/translations/de.json new file mode 100644 index 00000000000..443a5e3aa72 --- /dev/null +++ b/homeassistant/components/cloud/translations/de.json @@ -0,0 +1,15 @@ +{ + "system_health": { + "info": { + "alexa_enabled": "Alexa aktiviert", + "can_reach_cert_server": "Zertifikatsserver erreichbar", + "can_reach_cloud": "Home Assistant Cloud erreichbar", + "can_reach_cloud_auth": "Authentifizierungsserver erreichbar", + "google_enabled": "Google aktiviert", + "logged_in": "Angemeldet", + "remote_connected": "Remote verbunden", + "remote_enabled": "Remote aktiviert", + "subscription_expiration": "Ablauf des Abonnements" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloud/translations/fr.json b/homeassistant/components/cloud/translations/fr.json new file mode 100644 index 00000000000..9bb4029fce0 --- /dev/null +++ b/homeassistant/components/cloud/translations/fr.json @@ -0,0 +1,16 @@ +{ + "system_health": { + "info": { + "alexa_enabled": "Alexa activ\u00e9", + "can_reach_cert_server": "Acc\u00e9der au serveur de certificats", + "can_reach_cloud": "Acc\u00e9der \u00e0 Home Assistant Cloud", + "can_reach_cloud_auth": "Acc\u00e9der au serveur d'authentification", + "google_enabled": "Google activ\u00e9", + "logged_in": "Connect\u00e9", + "relayer_connected": "Relais connect\u00e9", + "remote_connected": "Contr\u00f4le \u00e0 distance connect\u00e9", + "remote_enabled": "Contr\u00f4le \u00e0 distance activ\u00e9", + "subscription_expiration": "Expiration de l'abonnement" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloud/translations/pl.json b/homeassistant/components/cloud/translations/pl.json index 30aaeeb77d1..1df32a14d8e 100644 --- a/homeassistant/components/cloud/translations/pl.json +++ b/homeassistant/components/cloud/translations/pl.json @@ -4,7 +4,7 @@ "alexa_enabled": "Alexa w\u0142\u0105czona", "can_reach_cert_server": "Dost\u0119p do serwera certyfikat\u00f3w", "can_reach_cloud": "Dost\u0119p do chmury Home Assistant", - "can_reach_cloud_auth": "Dost\u0119p do serwera uwierzytelniania", + "can_reach_cloud_auth": "Dost\u0119p do serwera certyfikat\u00f3w", "google_enabled": "Asystent Google w\u0142\u0105czony", "logged_in": "Zalogowany", "relayer_connected": "Relayer pod\u0142\u0105czony", diff --git a/homeassistant/components/cloud/translations/tr.json b/homeassistant/components/cloud/translations/tr.json index 0acb1e6a9a6..75d1c768beb 100644 --- a/homeassistant/components/cloud/translations/tr.json +++ b/homeassistant/components/cloud/translations/tr.json @@ -1,6 +1,9 @@ { "system_health": { "info": { + "alexa_enabled": "Alexa Etkin", + "can_reach_cloud": "Home Assistant Cloud'a ula\u015f\u0131n", + "google_enabled": "Google Etkin", "logged_in": "Giri\u015f Yapt\u0131", "relayer_connected": "Yeniden Katman ba\u011fl\u0131", "remote_connected": "Uzaktan Ba\u011fl\u0131", diff --git a/homeassistant/components/cloud/translations/uk.json b/homeassistant/components/cloud/translations/uk.json new file mode 100644 index 00000000000..a2e68b911e5 --- /dev/null +++ b/homeassistant/components/cloud/translations/uk.json @@ -0,0 +1,16 @@ +{ + "system_health": { + "info": { + "alexa_enabled": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f \u0437 Alexa", + "can_reach_cert_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0456\u0432", + "can_reach_cloud": "\u0414\u043e\u0441\u0442\u0443\u043f \u0434\u043e Home Assistant Cloud", + "can_reach_cloud_auth": "\u0414\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457", + "google_enabled": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f \u0437 Google", + "logged_in": "\u0412\u0445\u0456\u0434 \u0443 \u0441\u0438\u0441\u0442\u0435\u043c\u0443", + "relayer_connected": "Relayer \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439", + "remote_connected": "\u0412\u0456\u0434\u0434\u0430\u043b\u0435\u043d\u0438\u0439 \u0434\u043e\u0441\u0442\u0443\u043f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439", + "remote_enabled": "\u0412\u0456\u0434\u0434\u0430\u043b\u0435\u043d\u0438\u0439 \u0434\u043e\u0441\u0442\u0443\u043f \u0430\u043a\u0442\u0438\u0432\u043e\u0432\u0430\u043d\u0438\u0439", + "subscription_expiration": "\u0422\u0435\u0440\u043c\u0456\u043d \u0434\u0456\u0457 \u043f\u0435\u0440\u0435\u0434\u043f\u043b\u0430\u0442\u0438" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cloud/tts.py b/homeassistant/components/cloud/tts.py index 9dd392a12c5..4d19547d30c 100644 --- a/homeassistant/components/cloud/tts.py +++ b/homeassistant/components/cloud/tts.py @@ -12,13 +12,14 @@ CONF_GENDER = "gender" SUPPORT_LANGUAGES = list({key[0] for key in MAP_VOICE}) -DEFAULT_LANG = "en-US" -DEFAULT_GENDER = "female" - def validate_lang(value): """Validate chosen gender or language.""" - lang = value[CONF_LANG] + lang = value.get(CONF_LANG) + + if lang is None: + return value + gender = value.get(CONF_GENDER) if gender is None: @@ -35,7 +36,7 @@ def validate_lang(value): PLATFORM_SCHEMA = vol.All( PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_LANG, default=DEFAULT_LANG): str, + vol.Optional(CONF_LANG): str, vol.Optional(CONF_GENDER): str, } ), @@ -48,8 +49,8 @@ async def async_get_engine(hass, config, discovery_info=None): cloud: Cloud = hass.data[DOMAIN] if discovery_info is not None: - language = DEFAULT_LANG - gender = DEFAULT_GENDER + language = None + gender = None else: language = config[CONF_LANG] gender = config[CONF_GENDER] @@ -67,6 +68,16 @@ class CloudProvider(Provider): self._language = language self._gender = gender + if self._language is not None: + return + + self._language, self._gender = cloud.client.prefs.tts_default_voice + cloud.client.prefs.async_listen_updates(self._sync_prefs) + + async def _sync_prefs(self, prefs): + """Sync preferences.""" + self._language, self._gender = prefs.tts_default_voice + @property def default_language(self): """Return the default language.""" diff --git a/homeassistant/components/cloudflare/translations/de.json b/homeassistant/components/cloudflare/translations/de.json index 809dad5da46..d9858b36f55 100644 --- a/homeassistant/components/cloudflare/translations/de.json +++ b/homeassistant/components/cloudflare/translations/de.json @@ -1,12 +1,15 @@ { "config": { "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", "unknown": "Unerwarteter Fehler" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "invalid_zone": "Ung\u00fcltige Zone" }, + "flow_title": "Cloudflare: {name}", "step": { "records": { "data": { @@ -18,6 +21,11 @@ "api_token": "API Token" }, "title": "Mit Cloudflare verbinden" + }, + "zone": { + "data": { + "zone": "Zone" + } } } } diff --git a/homeassistant/components/cloudflare/translations/tr.json b/homeassistant/components/cloudflare/translations/tr.json index b7c7b438804..5d1180961f6 100644 --- a/homeassistant/components/cloudflare/translations/tr.json +++ b/homeassistant/components/cloudflare/translations/tr.json @@ -1,6 +1,12 @@ { "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "unknown": "Beklenmeyen hata" + }, "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "invalid_zone": "Ge\u00e7ersiz b\u00f6lge" }, "flow_title": "Cloudflare: {name}", @@ -12,6 +18,9 @@ "title": "G\u00fcncellenecek Kay\u0131tlar\u0131 Se\u00e7in" }, "user": { + "data": { + "api_token": "API Belirteci" + }, "title": "Cloudflare'ye ba\u011flan\u0131n" }, "zone": { diff --git a/homeassistant/components/cloudflare/translations/uk.json b/homeassistant/components/cloudflare/translations/uk.json new file mode 100644 index 00000000000..425ec2733b8 --- /dev/null +++ b/homeassistant/components/cloudflare/translations/uk.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "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.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "invalid_zone": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0437\u043e\u043d\u0430" + }, + "flow_title": "Cloudflare: {name}", + "step": { + "records": { + "data": { + "records": "\u0417\u0430\u043f\u0438\u0441\u0438" + }, + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0437\u0430\u043f\u0438\u0441\u0438 \u0434\u043b\u044f \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f" + }, + "user": { + "data": { + "api_token": "\u0422\u043e\u043a\u0435\u043d API" + }, + "description": "\u0414\u043b\u044f \u0446\u0456\u0454\u0457 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0442\u043e\u043a\u0435\u043d API, \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u0438\u0439 \u0437 \u0434\u043e\u0437\u0432\u043e\u043b\u0430\u043c\u0438 Zone: Zone: Read \u0456 Zone: DNS: Edit \u0434\u043b\u044f \u0432\u0441\u0456\u0445 \u0437\u043e\u043d \u0443 \u0432\u0430\u0448\u043e\u043c\u0443 \u043f\u0440\u043e\u0444\u0456\u043b\u0456.", + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e Cloudflare" + }, + "zone": { + "data": { + "zone": "\u0417\u043e\u043d\u0430" + }, + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0437\u043e\u043d\u0443 \u0434\u043b\u044f \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/comfoconnect/manifest.json b/homeassistant/components/comfoconnect/manifest.json index 966de82f219..8488ef58f1f 100644 --- a/homeassistant/components/comfoconnect/manifest.json +++ b/homeassistant/components/comfoconnect/manifest.json @@ -2,6 +2,6 @@ "domain": "comfoconnect", "name": "Zehnder ComfoAir Q", "documentation": "https://www.home-assistant.io/integrations/comfoconnect", - "requirements": ["pycomfoconnect==0.3"], + "requirements": ["pycomfoconnect==0.4"], "codeowners": ["@michaelarnauts"] } diff --git a/homeassistant/components/comfoconnect/sensor.py b/homeassistant/components/comfoconnect/sensor.py index 8b34fe1fd72..53075beecaf 100644 --- a/homeassistant/components/comfoconnect/sensor.py +++ b/homeassistant/components/comfoconnect/sensor.py @@ -3,6 +3,7 @@ import logging from pycomfoconnect import ( SENSOR_BYPASS_STATE, + SENSOR_CURRENT_RMOT, SENSOR_DAYS_TO_REPLACE_FILTER, SENSOR_FAN_EXHAUST_DUTY, SENSOR_FAN_EXHAUST_FLOW, @@ -15,6 +16,9 @@ from pycomfoconnect import ( SENSOR_HUMIDITY_OUTDOOR, SENSOR_HUMIDITY_SUPPLY, SENSOR_POWER_CURRENT, + SENSOR_POWER_TOTAL, + SENSOR_PREHEATER_POWER_CURRENT, + SENSOR_PREHEATER_POWER_TOTAL, SENSOR_TEMPERATURE_EXHAUST, SENSOR_TEMPERATURE_EXTRACT, SENSOR_TEMPERATURE_OUTDOOR, @@ -26,9 +30,11 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( ATTR_DEVICE_CLASS, CONF_RESOURCES, + DEVICE_CLASS_ENERGY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, + ENERGY_KILO_WATT_HOUR, PERCENTAGE, POWER_WATT, TEMP_CELSIUS, @@ -45,6 +51,7 @@ ATTR_AIR_FLOW_EXHAUST = "air_flow_exhaust" ATTR_AIR_FLOW_SUPPLY = "air_flow_supply" ATTR_BYPASS_STATE = "bypass_state" ATTR_CURRENT_HUMIDITY = "current_humidity" +ATTR_CURRENT_RMOT = "current_rmot" ATTR_CURRENT_TEMPERATURE = "current_temperature" ATTR_DAYS_TO_REPLACE_FILTER = "days_to_replace_filter" ATTR_EXHAUST_FAN_DUTY = "exhaust_fan_duty" @@ -54,6 +61,9 @@ ATTR_EXHAUST_TEMPERATURE = "exhaust_temperature" ATTR_OUTSIDE_HUMIDITY = "outside_humidity" ATTR_OUTSIDE_TEMPERATURE = "outside_temperature" ATTR_POWER_CURRENT = "power_usage" +ATTR_POWER_TOTAL = "power_total" +ATTR_PREHEATER_POWER_CURRENT = "preheater_power_usage" +ATTR_PREHEATER_POWER_TOTAL = "preheater_power_total" ATTR_SUPPLY_FAN_DUTY = "supply_fan_duty" ATTR_SUPPLY_FAN_SPEED = "supply_fan_speed" ATTR_SUPPLY_HUMIDITY = "supply_humidity" @@ -72,7 +82,7 @@ SENSOR_TYPES = { ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_LABEL: "Inside Temperature", ATTR_UNIT: TEMP_CELSIUS, - ATTR_ICON: "mdi:thermometer", + ATTR_ICON: None, ATTR_ID: SENSOR_TEMPERATURE_EXTRACT, ATTR_MULTIPLIER: 0.1, }, @@ -80,14 +90,22 @@ SENSOR_TYPES = { ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, ATTR_LABEL: "Inside Humidity", ATTR_UNIT: PERCENTAGE, - ATTR_ICON: "mdi:water-percent", + ATTR_ICON: None, ATTR_ID: SENSOR_HUMIDITY_EXTRACT, }, + ATTR_CURRENT_RMOT: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_LABEL: "Current RMOT", + ATTR_UNIT: TEMP_CELSIUS, + ATTR_ICON: None, + ATTR_ID: SENSOR_CURRENT_RMOT, + ATTR_MULTIPLIER: 0.1, + }, ATTR_OUTSIDE_TEMPERATURE: { ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_LABEL: "Outside Temperature", ATTR_UNIT: TEMP_CELSIUS, - ATTR_ICON: "mdi:thermometer", + ATTR_ICON: None, ATTR_ID: SENSOR_TEMPERATURE_OUTDOOR, ATTR_MULTIPLIER: 0.1, }, @@ -95,14 +113,14 @@ SENSOR_TYPES = { ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, ATTR_LABEL: "Outside Humidity", ATTR_UNIT: PERCENTAGE, - ATTR_ICON: "mdi:water-percent", + ATTR_ICON: None, ATTR_ID: SENSOR_HUMIDITY_OUTDOOR, }, ATTR_SUPPLY_TEMPERATURE: { ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_LABEL: "Supply Temperature", ATTR_UNIT: TEMP_CELSIUS, - ATTR_ICON: "mdi:thermometer", + ATTR_ICON: None, ATTR_ID: SENSOR_TEMPERATURE_SUPPLY, ATTR_MULTIPLIER: 0.1, }, @@ -110,7 +128,7 @@ SENSOR_TYPES = { ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, ATTR_LABEL: "Supply Humidity", ATTR_UNIT: PERCENTAGE, - ATTR_ICON: "mdi:water-percent", + ATTR_ICON: None, ATTR_ID: SENSOR_HUMIDITY_SUPPLY, }, ATTR_SUPPLY_FAN_SPEED: { @@ -145,7 +163,7 @@ SENSOR_TYPES = { ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, ATTR_LABEL: "Exhaust Temperature", ATTR_UNIT: TEMP_CELSIUS, - ATTR_ICON: "mdi:thermometer", + ATTR_ICON: None, ATTR_ID: SENSOR_TEMPERATURE_EXHAUST, ATTR_MULTIPLIER: 0.1, }, @@ -153,7 +171,7 @@ SENSOR_TYPES = { ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, ATTR_LABEL: "Exhaust Humidity", ATTR_UNIT: PERCENTAGE, - ATTR_ICON: "mdi:water-percent", + ATTR_ICON: None, ATTR_ID: SENSOR_HUMIDITY_EXHAUST, }, ATTR_AIR_FLOW_SUPPLY: { @@ -188,9 +206,30 @@ SENSOR_TYPES = { ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, ATTR_LABEL: "Power usage", ATTR_UNIT: POWER_WATT, - ATTR_ICON: "mdi:flash", + ATTR_ICON: None, ATTR_ID: SENSOR_POWER_CURRENT, }, + ATTR_POWER_TOTAL: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_LABEL: "Power total", + ATTR_UNIT: ENERGY_KILO_WATT_HOUR, + ATTR_ICON: None, + ATTR_ID: SENSOR_POWER_TOTAL, + }, + ATTR_PREHEATER_POWER_CURRENT: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_POWER, + ATTR_LABEL: "Preheater power usage", + ATTR_UNIT: POWER_WATT, + ATTR_ICON: None, + ATTR_ID: SENSOR_PREHEATER_POWER_CURRENT, + }, + ATTR_PREHEATER_POWER_TOTAL: { + ATTR_DEVICE_CLASS: DEVICE_CLASS_ENERGY, + ATTR_LABEL: "Preheater power total", + ATTR_UNIT: ENERGY_KILO_WATT_HOUR, + ATTR_ICON: None, + ATTR_ID: SENSOR_PREHEATER_POWER_TOTAL, + }, } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( diff --git a/homeassistant/components/config/config_entries.py b/homeassistant/components/config/config_entries.py index f67bfb98641..b8d9944d7af 100644 --- a/homeassistant/components/config/config_entries.py +++ b/homeassistant/components/config/config_entries.py @@ -318,7 +318,9 @@ async def config_entry_update(hass, connection, msg): @websocket_api.require_admin @websocket_api.async_response -@websocket_api.websocket_command({"type": "config_entries/ignore_flow", "flow_id": str}) +@websocket_api.websocket_command( + {"type": "config_entries/ignore_flow", "flow_id": str, "title": str} +) async def ignore_config_flow(hass, connection, msg): """Ignore a config flow.""" flow = next( @@ -345,7 +347,7 @@ async def ignore_config_flow(hass, connection, msg): await hass.config_entries.flow.async_init( flow["handler"], context={"source": config_entries.SOURCE_IGNORE}, - data={"unique_id": flow["context"]["unique_id"]}, + data={"unique_id": flow["context"]["unique_id"], "title": msg["title"]}, ) connection.send_result(msg["id"]) diff --git a/homeassistant/components/config/entity_registry.py b/homeassistant/components/config/entity_registry.py index 8d1c488bfa0..f0ee30ca120 100644 --- a/homeassistant/components/config/entity_registry.py +++ b/homeassistant/components/config/entity_registry.py @@ -102,7 +102,9 @@ async def websocket_update_entity(hass, connection, msg): if hass.states.get(msg["new_entity_id"]) is not None: connection.send_message( websocket_api.error_message( - msg["id"], "invalid_info", "Entity is already registered" + msg["id"], + "invalid_info", + "Entity with this ID is already registered", ) ) return diff --git a/homeassistant/components/control4/translations/de.json b/homeassistant/components/control4/translations/de.json index f9a5783cd91..399b8d42491 100644 --- a/homeassistant/components/control4/translations/de.json +++ b/homeassistant/components/control4/translations/de.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/control4/translations/tr.json b/homeassistant/components/control4/translations/tr.json new file mode 100644 index 00000000000..aed7e564a76 --- /dev/null +++ b/homeassistant/components/control4/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "\u0130p Adresi", + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/control4/translations/uk.json b/homeassistant/components/control4/translations/uk.json index 6c0426eba8f..682d86c5deb 100644 --- a/homeassistant/components/control4/translations/uk.json +++ b/homeassistant/components/control4/translations/uk.json @@ -1,10 +1,21 @@ { "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, "step": { "user": { "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" - } + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0434\u0430\u043d\u0456 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 Control4 \u0456 IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u0412\u0430\u0448\u043e\u0433\u043e \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430." } } }, @@ -12,7 +23,7 @@ "step": { "init": { "data": { - "scan_interval": "\u0421\u0435\u043a\u0443\u043d\u0434 \u043c\u0456\u0436 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f\u043c\u0438" + "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" } } } diff --git a/homeassistant/components/coolmaster/translations/de.json b/homeassistant/components/coolmaster/translations/de.json index 908dfaa448c..4e58b1ed964 100644 --- a/homeassistant/components/coolmaster/translations/de.json +++ b/homeassistant/components/coolmaster/translations/de.json @@ -1,7 +1,7 @@ { "config": { "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", "no_units": "Es wurden keine HVAC-Ger\u00e4te im CoolMasterNet-Host gefunden." }, "step": { diff --git a/homeassistant/components/coolmaster/translations/tr.json b/homeassistant/components/coolmaster/translations/tr.json new file mode 100644 index 00000000000..4848a34362c --- /dev/null +++ b/homeassistant/components/coolmaster/translations/tr.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "off": "Kapat\u0131labilir" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coolmaster/translations/uk.json b/homeassistant/components/coolmaster/translations/uk.json new file mode 100644 index 00000000000..038a7bc48f0 --- /dev/null +++ b/homeassistant/components/coolmaster/translations/uk.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "no_units": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u043d\u0430\u0439\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043e\u043f\u0430\u043b\u0435\u043d\u043d\u044f, \u0432\u0435\u043d\u0442\u0438\u043b\u044f\u0446\u0456\u0457 \u0442\u0430 \u043a\u043e\u043d\u0434\u0438\u0446\u0456\u043e\u043d\u0443\u0432\u0430\u043d\u043d\u044f." + }, + "step": { + "user": { + "data": { + "cool": "\u0420\u0435\u0436\u0438\u043c \u043e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f", + "dry": "\u0420\u0435\u0436\u0438\u043c \u043e\u0441\u0443\u0448\u0435\u043d\u043d\u044f", + "fan_only": "\u0420\u0435\u0436\u0438\u043c \u0432\u0435\u043d\u0442\u0438\u043b\u044f\u0446\u0456\u0457", + "heat": "\u0420\u0435\u0436\u0438\u043c \u043e\u0431\u0456\u0433\u0440\u0456\u0432\u0443", + "heat_cool": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c", + "host": "\u0425\u043e\u0441\u0442", + "off": "\u0414\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0438 \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f" + }, + "title": "CoolMasterNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coronavirus/translations/tr.json b/homeassistant/components/coronavirus/translations/tr.json new file mode 100644 index 00000000000..b608d60f824 --- /dev/null +++ b/homeassistant/components/coronavirus/translations/tr.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "user": { + "data": { + "country": "\u00dclke" + }, + "title": "\u0130zlemek i\u00e7in bir \u00fclke se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/coronavirus/translations/uk.json b/homeassistant/components/coronavirus/translations/uk.json new file mode 100644 index 00000000000..151e7b14d3f --- /dev/null +++ b/homeassistant/components/coronavirus/translations/uk.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "step": { + "user": { + "data": { + "country": "\u041a\u0440\u0430\u0457\u043d\u0430" + }, + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043a\u0440\u0430\u0457\u043d\u0443 \u0434\u043b\u044f \u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433\u0443" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/translations/tr.json b/homeassistant/components/cover/translations/tr.json index 98bc8cdb18d..f042233a6d1 100644 --- a/homeassistant/components/cover/translations/tr.json +++ b/homeassistant/components/cover/translations/tr.json @@ -1,4 +1,10 @@ { + "device_automation": { + "action_type": { + "close": "{entity_name} kapat", + "open": "{entity_name} a\u00e7\u0131n" + } + }, "state": { "_": { "closed": "Kapal\u0131", diff --git a/homeassistant/components/cover/translations/uk.json b/homeassistant/components/cover/translations/uk.json index 66cd0c77c73..ceb49fff3e9 100644 --- a/homeassistant/components/cover/translations/uk.json +++ b/homeassistant/components/cover/translations/uk.json @@ -1,10 +1,29 @@ { "device_automation": { "action_type": { + "close": "{entity_name}: \u0437\u0430\u043a\u0440\u0438\u0442\u0438", + "close_tilt": "{entity_name}: \u0437\u0430\u043a\u0440\u0438\u0442\u0438 \u043b\u0430\u043c\u0435\u043b\u0456", + "open": "{entity_name}: \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u0438", + "open_tilt": "{entity_name}: \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u0438 \u043b\u0430\u043c\u0435\u043b\u0456", + "set_position": "{entity_name}: \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0438 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u043d\u044f", + "set_tilt_position": "{entity_name}: \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0438 \u043d\u0430\u0445\u0438\u043b \u043b\u0430\u043c\u0435\u043b\u0435\u0439", "stop": "\u0417\u0443\u043f\u0438\u043d\u0438\u0442\u0438 {entity_name}" }, + "condition_type": { + "is_closed": "{entity_name} \u0432 \u0437\u0430\u043a\u0440\u0438\u0442\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_closing": "{entity_name} \u0437\u0430\u043a\u0440\u0438\u0432\u0430\u0454\u0442\u044c\u0441\u044f", + "is_open": "{entity_name} \u0443 \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_opening": "{entity_name} \u0432\u0456\u0434\u043a\u0440\u0438\u0432\u0430\u0454\u0442\u044c\u0441\u044f", + "is_position": "{entity_name} \u0437\u043d\u0430\u0445\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0432 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u043d\u0456", + "is_tilt_position": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \"{entity_name}\" \u043c\u0430\u0454 \u043d\u0430\u0445\u0438\u043b \u043b\u0430\u043c\u0435\u043b\u0435\u0439" + }, "trigger_type": { - "opened": "{entity_name} \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u043e" + "closed": "{entity_name} \u0437\u0430\u043a\u0440\u0438\u0442\u043e", + "closing": "{entity_name} \u0437\u0430\u043a\u0440\u0438\u0432\u0430\u0454\u0442\u044c\u0441\u044f", + "opened": "{entity_name} \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u043e", + "opening": "{entity_name} \u0432\u0456\u0434\u043a\u0440\u0438\u0432\u0430\u0454\u0442\u044c\u0441\u044f", + "position": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u043f\u043e\u043b\u043e\u0436\u0435\u043d\u043d\u044f", + "tilt_position": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u043d\u0430\u0445\u0438\u043b \u043b\u0430\u043c\u0435\u043b\u0435\u0439" } }, "state": { diff --git a/homeassistant/components/daikin/manifest.json b/homeassistant/components/daikin/manifest.json index ebf31967cc7..245f10a0e83 100644 --- a/homeassistant/components/daikin/manifest.json +++ b/homeassistant/components/daikin/manifest.json @@ -3,7 +3,7 @@ "name": "Daikin AC", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/daikin", - "requirements": ["pydaikin==2.4.0"], + "requirements": ["pydaikin==2.4.1"], "codeowners": ["@fredrike"], "zeroconf": ["_dkapi._tcp.local."], "quality_scale": "platinum" diff --git a/homeassistant/components/daikin/translations/de.json b/homeassistant/components/daikin/translations/de.json index bbac113eb44..dcec53c1569 100644 --- a/homeassistant/components/daikin/translations/de.json +++ b/homeassistant/components/daikin/translations/de.json @@ -2,15 +2,17 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { "user": { "data": { + "api_key": "API-Schl\u00fcssel", "host": "Host", "password": "Passwort" }, diff --git a/homeassistant/components/daikin/translations/pt.json b/homeassistant/components/daikin/translations/pt.json index dd9b538ae8b..d4188fb10f9 100644 --- a/homeassistant/components/daikin/translations/pt.json +++ b/homeassistant/components/daikin/translations/pt.json @@ -16,7 +16,7 @@ "host": "Servidor", "password": "Palavra-passe" }, - "description": "Introduza o endere\u00e7o IP do seu Daikin AC.", + "description": "Introduza Endere\u00e7o IP do seu Daikin AC.\n\nAten\u00e7\u00e3o que [%chave:common::config_flow::data::api_key%] e Palavra-passe s\u00f3 s\u00e3o utilizador pelos dispositivos BRP072Cxx e SKYFi, respectivamente.", "title": "Configurar o Daikin AC" } } diff --git a/homeassistant/components/daikin/translations/tr.json b/homeassistant/components/daikin/translations/tr.json new file mode 100644 index 00000000000..4148bf2b9f1 --- /dev/null +++ b/homeassistant/components/daikin/translations/tr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "host": "Ana Bilgisayar", + "password": "Parola" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/daikin/translations/uk.json b/homeassistant/components/daikin/translations/uk.json new file mode 100644 index 00000000000..648d68d7a81 --- /dev/null +++ b/homeassistant/components/daikin/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441\u0430 \u0412\u0430\u0448\u043e\u0433\u043e Daikin AC. \n\n\u0417\u0432\u0435\u0440\u043d\u0456\u0442\u044c \u0443\u0432\u0430\u0433\u0443, \u0449\u043e \u041a\u043b\u044e\u0447 API \u0456 \u041f\u0430\u0440\u043e\u043b\u044c \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u044e\u0442\u044c\u0441\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044f\u043c\u0438 BRP072Cxx \u0456 SKYFi \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u043d\u043e.", + "title": "Daikin AC" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/debugpy/manifest.json b/homeassistant/components/debugpy/manifest.json index 27b110b1f68..67af8fc553b 100644 --- a/homeassistant/components/debugpy/manifest.json +++ b/homeassistant/components/debugpy/manifest.json @@ -2,7 +2,7 @@ "domain": "debugpy", "name": "Remote Python Debugger", "documentation": "https://www.home-assistant.io/integrations/debugpy", - "requirements": ["debugpy==1.2.0"], + "requirements": ["debugpy==1.2.1"], "codeowners": ["@frenck"], "quality_scale": "internal" } diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 6c2df3ad614..bc14af9ff11 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -174,6 +174,18 @@ class DeconzFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=self.bridge_id, data=self.deconz_config) + async def async_step_reauth(self, config: dict): + """Trigger a reauthentication flow.""" + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context["title_placeholders"] = {CONF_HOST: config[CONF_HOST]} + + self.deconz_config = { + CONF_HOST: config[CONF_HOST], + CONF_PORT: config[CONF_PORT], + } + + return await self.async_step_link() + async def async_step_ssdp(self, discovery_info): """Handle a discovered deCONZ bridge.""" if ( diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 869a6ef3594..5ee0a00f04f 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -75,6 +75,7 @@ CONF_SIDE_6 = "side_6" HUE_DIMMER_REMOTE_MODEL_GEN1 = "RWL020" HUE_DIMMER_REMOTE_MODEL_GEN2 = "RWL021" +HUE_DIMMER_REMOTE_MODEL_GEN3 = "RWL022" HUE_DIMMER_REMOTE = { (CONF_SHORT_PRESS, CONF_TURN_ON): {CONF_EVENT: 1000}, (CONF_SHORT_RELEASE, CONF_TURN_ON): {CONF_EVENT: 1002}, @@ -362,6 +363,7 @@ AQARA_OPPLE_6_BUTTONS = { REMOTES = { HUE_DIMMER_REMOTE_MODEL_GEN1: HUE_DIMMER_REMOTE, HUE_DIMMER_REMOTE_MODEL_GEN2: HUE_DIMMER_REMOTE, + HUE_DIMMER_REMOTE_MODEL_GEN3: HUE_DIMMER_REMOTE, HUE_BUTTON_REMOTE_MODEL: HUE_BUTTON_REMOTE, HUE_TAP_REMOTE_MODEL: HUE_TAP_REMOTE, FRIENDS_OF_HUE_SWITCH_MODEL: FRIENDS_OF_HUE_SWITCH, @@ -417,7 +419,15 @@ async def async_validate_trigger_config(hass, config): or device.model not in REMOTES or trigger not in REMOTES[device.model] ): - raise InvalidDeviceAutomationConfig + if not device: + raise InvalidDeviceAutomationConfig( + f"deCONZ trigger {trigger} device with id " + f"{config[CONF_DEVICE_ID]} not found" + ) + raise InvalidDeviceAutomationConfig( + f"deCONZ trigger {trigger} is not valid for device " + f"{device} ({config[CONF_DEVICE_ID]})" + ) return config diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 881ea883c4c..a6cbb2acef9 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -4,6 +4,7 @@ import asyncio import async_timeout from pydeconz import DeconzSession, errors +from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady @@ -19,7 +20,7 @@ from .const import ( DEFAULT_ALLOW_CLIP_SENSOR, DEFAULT_ALLOW_DECONZ_GROUPS, DEFAULT_ALLOW_NEW_DEVICES, - DOMAIN, + DOMAIN as DECONZ_DOMAIN, LOGGER, NEW_GROUP, NEW_LIGHT, @@ -34,7 +35,7 @@ from .errors import AuthenticationRequired, CannotConnect @callback def get_gateway_from_config_entry(hass, config_entry): """Return gateway with a matching bridge id.""" - return hass.data[DOMAIN][config_entry.unique_id] + return hass.data[DECONZ_DOMAIN][config_entry.unique_id] class DeconzGateway: @@ -152,7 +153,7 @@ class DeconzGateway: # Gateway service device_registry.async_get_or_create( config_entry_id=self.config_entry.entry_id, - identifiers={(DOMAIN, self.api.config.bridgeid)}, + identifiers={(DECONZ_DOMAIN, self.api.config.bridgeid)}, manufacturer="Dresden Elektronik", model=self.api.config.modelid, name=self.api.config.name, @@ -173,8 +174,14 @@ class DeconzGateway: except CannotConnect as err: raise ConfigEntryNotReady from err - except Exception as err: # pylint: disable=broad-except - LOGGER.error("Error connecting with deCONZ gateway: %s", err, exc_info=True) + except AuthenticationRequired: + self.hass.async_create_task( + self.hass.config_entries.flow.async_init( + DECONZ_DOMAIN, + context={"source": SOURCE_REAUTH}, + data=self.config_entry.data, + ) + ) return False for component in SUPPORTED_PLATFORMS: diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 6d759ccaf48..9080160c76f 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -1,4 +1,7 @@ """Support for deCONZ lights.""" + +from pydeconz.light import Light + from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, @@ -105,21 +108,22 @@ class DeconzBaseLight(DeconzDevice, LightEntity): super().__init__(device, gateway) self._features = 0 + self.update_features(self._device) - if self._device.brightness is not None: + def update_features(self, device): + """Calculate supported features of device.""" + if device.brightness is not None: self._features |= SUPPORT_BRIGHTNESS self._features |= SUPPORT_FLASH self._features |= SUPPORT_TRANSITION - if self._device.ct is not None: + if device.ct is not None: self._features |= SUPPORT_COLOR_TEMP - if self._device.xy is not None or ( - self._device.hue is not None and self._device.sat is not None - ): + if device.xy is not None or (device.hue is not None and device.sat is not None): self._features |= SUPPORT_COLOR - if self._device.effect is not None: + if device.effect is not None: self._features |= SUPPORT_EFFECT @property @@ -146,7 +150,8 @@ class DeconzBaseLight(DeconzDevice, LightEntity): if self._device.colormode in ("xy", "hs"): if self._device.xy: return color_util.color_xy_to_hs(*self._device.xy) - return (self._device.hue / 65535 * 360, self._device.sat / 255 * 100) + if self._device.hue and self._device.sat: + return (self._device.hue / 65535 * 360, self._device.sat / 255 * 100) return None @property @@ -250,6 +255,11 @@ class DeconzGroup(DeconzBaseLight): super().__init__(device, gateway) + for light_id in device.lights: + light = gateway.api.lights[light_id] + if light.ZHATYPE == Light.ZHATYPE: + self.update_features(light) + @property def unique_id(self): """Return a unique identifier for this device.""" diff --git a/homeassistant/components/deconz/logbook.py b/homeassistant/components/deconz/logbook.py new file mode 100644 index 00000000000..73c157ac8f6 --- /dev/null +++ b/homeassistant/components/deconz/logbook.py @@ -0,0 +1,143 @@ +"""Describe deCONZ logbook events.""" + +from typing import Callable, Optional + +from homeassistant.const import ATTR_DEVICE_ID, CONF_EVENT +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.event import Event + +from .const import DOMAIN as DECONZ_DOMAIN +from .deconz_event import CONF_DECONZ_EVENT, DeconzEvent +from .device_trigger import ( + CONF_BOTH_BUTTONS, + CONF_BOTTOM_BUTTONS, + CONF_BUTTON_1, + CONF_BUTTON_2, + CONF_BUTTON_3, + CONF_BUTTON_4, + CONF_CLOSE, + CONF_DIM_DOWN, + CONF_DIM_UP, + CONF_DOUBLE_PRESS, + CONF_DOUBLE_TAP, + CONF_LEFT, + CONF_LONG_PRESS, + CONF_LONG_RELEASE, + CONF_MOVE, + CONF_OPEN, + CONF_QUADRUPLE_PRESS, + CONF_QUINTUPLE_PRESS, + CONF_RIGHT, + CONF_ROTATE_FROM_SIDE_1, + CONF_ROTATE_FROM_SIDE_2, + CONF_ROTATE_FROM_SIDE_3, + CONF_ROTATE_FROM_SIDE_4, + CONF_ROTATE_FROM_SIDE_5, + CONF_ROTATE_FROM_SIDE_6, + CONF_ROTATED, + CONF_ROTATED_FAST, + CONF_ROTATION_STOPPED, + CONF_SHAKE, + CONF_SHORT_PRESS, + CONF_SHORT_RELEASE, + CONF_SIDE_1, + CONF_SIDE_2, + CONF_SIDE_3, + CONF_SIDE_4, + CONF_SIDE_5, + CONF_SIDE_6, + CONF_TOP_BUTTONS, + CONF_TRIPLE_PRESS, + CONF_TURN_OFF, + CONF_TURN_ON, + REMOTES, + _get_deconz_event_from_device_id, +) + +ACTIONS = { + CONF_SHORT_PRESS: "Short press", + CONF_SHORT_RELEASE: "Short release", + CONF_LONG_PRESS: "Long press", + CONF_LONG_RELEASE: "Long release", + CONF_DOUBLE_PRESS: "Double press", + CONF_TRIPLE_PRESS: "Triple press", + CONF_QUADRUPLE_PRESS: "Quadruple press", + CONF_QUINTUPLE_PRESS: "Quintuple press", + CONF_ROTATED: "Rotated", + CONF_ROTATED_FAST: "Rotated fast", + CONF_ROTATION_STOPPED: "Rotated stopped", + CONF_MOVE: "Move", + CONF_DOUBLE_TAP: "Double tap", + CONF_SHAKE: "Shake", + CONF_ROTATE_FROM_SIDE_1: "Rotate from side 1", + CONF_ROTATE_FROM_SIDE_2: "Rotate from side 2", + CONF_ROTATE_FROM_SIDE_3: "Rotate from side 3", + CONF_ROTATE_FROM_SIDE_4: "Rotate from side 4", + CONF_ROTATE_FROM_SIDE_5: "Rotate from side 5", + CONF_ROTATE_FROM_SIDE_6: "Rotate from side 6", +} + +INTERFACES = { + CONF_TURN_ON: "Turn on", + CONF_TURN_OFF: "Turn off", + CONF_DIM_UP: "Dim up", + CONF_DIM_DOWN: "Dim down", + CONF_LEFT: "Left", + CONF_RIGHT: "Right", + CONF_OPEN: "Open", + CONF_CLOSE: "Close", + CONF_BOTH_BUTTONS: "Both buttons", + CONF_TOP_BUTTONS: "Top buttons", + CONF_BOTTOM_BUTTONS: "Bottom buttons", + CONF_BUTTON_1: "Button 1", + CONF_BUTTON_2: "Button 2", + CONF_BUTTON_3: "Button 3", + CONF_BUTTON_4: "Button 4", + CONF_SIDE_1: "Side 1", + CONF_SIDE_2: "Side 2", + CONF_SIDE_3: "Side 3", + CONF_SIDE_4: "Side 4", + CONF_SIDE_5: "Side 5", + CONF_SIDE_6: "Side 6", +} + + +def _get_device_event_description(modelid: str, event: str) -> tuple: + """Get device event description.""" + device_event_descriptions: dict = REMOTES[modelid] + + for event_type_tuple, event_dict in device_event_descriptions.items(): + if event == event_dict[CONF_EVENT]: + return event_type_tuple + + +@callback +def async_describe_events( + hass: HomeAssistant, + async_describe_event: Callable[[str, str, Callable[[Event], dict]], None], +) -> None: + """Describe logbook events.""" + + @callback + def async_describe_deconz_event(event: Event) -> dict: + """Describe deCONZ logbook event.""" + deconz_event: Optional[DeconzEvent] = _get_deconz_event_from_device_id( + hass, event.data[ATTR_DEVICE_ID] + ) + + if deconz_event.device.modelid not in REMOTES: + return { + "name": f"{deconz_event.device.name}", + "message": f"fired event '{event.data[CONF_EVENT]}'.", + } + + action, interface = _get_device_event_description( + deconz_event.device.modelid, event.data[CONF_EVENT] + ) + + return { + "name": f"{deconz_event.device.name}", + "message": f"'{ACTIONS[action]}' event for '{INTERFACES[interface]}' was fired.", + } + + async_describe_event(DECONZ_DOMAIN, CONF_DECONZ_EVENT, async_describe_deconz_event) diff --git a/homeassistant/components/deconz/translations/cs.json b/homeassistant/components/deconz/translations/cs.json index 3ad72dc9ac9..52cbd607b7f 100644 --- a/homeassistant/components/deconz/translations/cs.json +++ b/homeassistant/components/deconz/translations/cs.json @@ -60,14 +60,15 @@ }, "trigger_type": { "remote_awakened": "Za\u0159\u00edzen\u00ed probuzeno", - "remote_button_double_press": "Dvakr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\"", + "remote_button_double_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto dvakr\u00e1t", + "remote_button_long_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto dlouze", "remote_button_long_release": "Uvoln\u011bno tla\u010d\u00edtko \"{subtype}\" po dlouh\u00e9m stisku", - "remote_button_quadruple_press": "\u010cty\u0159ikr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\"", - "remote_button_quintuple_press": "P\u011btkr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\"", + "remote_button_quadruple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto \u010dty\u0159ikr\u00e1t", + "remote_button_quintuple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto p\u011btkr\u00e1t", "remote_button_rotation_stopped": "Oto\u010den\u00ed tla\u010d\u00edtka \"{subtype}\" bylo zastaveno", - "remote_button_short_press": "Stiknuto tla\u010d\u00edtko \"{subtype}\"", + "remote_button_short_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto", "remote_button_short_release": "Uvoln\u011bno tla\u010d\u00edtko \"{subtype}\"", - "remote_button_triple_press": "T\u0159ikr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\"", + "remote_button_triple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto t\u0159ikr\u00e1t", "remote_double_tap": "Dvakr\u00e1t poklep\u00e1no na za\u0159\u00edzen\u00ed \"{subtype}\"", "remote_double_tap_any_side": "Za\u0159\u00edzen\u00ed bylo poklep\u00e1no 2x na libovolnou stranu", "remote_flip_180_degrees": "Za\u0159\u00edzen\u00ed p\u0159evr\u00e1ceno o 180 stup\u0148\u016f", diff --git a/homeassistant/components/deconz/translations/de.json b/homeassistant/components/deconz/translations/de.json index f9448705c5d..d7553652412 100644 --- a/homeassistant/components/deconz/translations/de.json +++ b/homeassistant/components/deconz/translations/de.json @@ -2,8 +2,9 @@ "config": { "abort": { "already_configured": "Bridge ist bereits konfiguriert", - "already_in_progress": "Der Konfigurationsablauf f\u00fcr die Bridge wird bereits ausgef\u00fchrt.", - "no_bridges": "Keine deCON-Bridges entdeckt", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "no_bridges": "Keine deCONZ-Bridges entdeckt", + "no_hardware_available": "Keine Funkhardware an deCONZ angeschlossen", "not_deconz_bridge": "Keine deCONZ Bridge entdeckt", "updated_instance": "deCONZ-Instanz mit neuer Host-Adresse aktualisiert" }, @@ -13,7 +14,7 @@ "flow_title": "deCONZ Zigbee Gateway", "step": { "hassio_confirm": { - "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass er eine Verbindung mit dem deCONZ Gateway herstellt, der vom Add-on hass.io {addon} bereitgestellt wird?", + "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass er eine Verbindung mit dem deCONZ Gateway herstellt, der vom Hass.io Add-on {addon} bereitgestellt wird?", "title": "deCONZ Zigbee Gateway \u00fcber das Hass.io Add-on" }, "link": { @@ -28,7 +29,7 @@ }, "user": { "data": { - "host": "W\u00e4hlen Sie das erkannte deCONZ-Gateway aus" + "host": "W\u00e4hle das erkannte deCONZ-Gateway aus" } } } @@ -92,7 +93,8 @@ "deconz_devices": { "data": { "allow_clip_sensor": "deCONZ CLIP-Sensoren zulassen", - "allow_deconz_groups": "deCONZ-Lichtgruppen zulassen" + "allow_deconz_groups": "deCONZ-Lichtgruppen zulassen", + "allow_new_devices": "Automatisches Hinzuf\u00fcgen von neuen Ger\u00e4ten zulassen" }, "description": "Sichtbarkeit der deCONZ-Ger\u00e4tetypen konfigurieren", "title": "deCONZ-Optionen" diff --git a/homeassistant/components/deconz/translations/no.json b/homeassistant/components/deconz/translations/no.json index 48716379483..c1435dbb186 100644 --- a/homeassistant/components/deconz/translations/no.json +++ b/homeassistant/components/deconz/translations/no.json @@ -14,8 +14,8 @@ "flow_title": "", "step": { "hassio_confirm": { - "description": "Vil du konfigurere Home Assistant til \u00e5 koble seg til deCONZ-gateway levert av Hass.io-tillegget {addon} ?", - "title": "deCONZ Zigbee gateway via Hass.io tillegg" + "description": "Vil du konfigurere Home Assistant til \u00e5 koble seg til deCONZ-gateway levert av Hass.io-tillegg {addon} ?", + "title": "deCONZ Zigbee gateway via Hass.io-tillegg" }, "link": { "description": "L\u00e5s opp deCONZ-gatewayen din for \u00e5 registrere deg med Home Assistant. \n\n 1. G\u00e5 til deCONZ-systeminnstillinger -> Gateway -> Avansert \n 2. Trykk p\u00e5 \"Autentiser app\" knappen", diff --git a/homeassistant/components/deconz/translations/pl.json b/homeassistant/components/deconz/translations/pl.json index 24a3ba61706..1b4eba97096 100644 --- a/homeassistant/components/deconz/translations/pl.json +++ b/homeassistant/components/deconz/translations/pl.json @@ -38,9 +38,9 @@ "trigger_subtype": { "both_buttons": "oba przyciski", "bottom_buttons": "dolne przyciski", - "button_1": "pierwszy przycisk", - "button_2": "drugi przycisk", - "button_3": "trzeci przycisk", + "button_1": "pierwszy", + "button_2": "drugi", + "button_3": "trzeci", "button_4": "czwarty", "close": "zamknij", "dim_down": "zmniejszenie jasno\u015bci", diff --git a/homeassistant/components/deconz/translations/ru.json b/homeassistant/components/deconz/translations/ru.json index a6bc0daaa3e..f22975530d8 100644 --- a/homeassistant/components/deconz/translations/ru.json +++ b/homeassistant/components/deconz/translations/ru.json @@ -14,8 +14,8 @@ "flow_title": "\u0428\u043b\u044e\u0437 Zigbee deCONZ ({host})", "step": { "hassio_confirm": { - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a deCONZ (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io \"{addon}\")?", - "title": "Zigbee \u0448\u043b\u044e\u0437 deCONZ (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io)" + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a deCONZ (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant \"{addon}\")?", + "title": "Zigbee \u0448\u043b\u044e\u0437 deCONZ (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant)" }, "link": { "description": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0432 Home Assistant:\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u044b deCONZ -> Gateway -> Advanced.\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u00abAuthenticate app\u00bb.", diff --git a/homeassistant/components/deconz/translations/tr.json b/homeassistant/components/deconz/translations/tr.json index e73703043f3..22eea1278d7 100644 --- a/homeassistant/components/deconz/translations/tr.json +++ b/homeassistant/components/deconz/translations/tr.json @@ -1,4 +1,47 @@ { + "config": { + "abort": { + "already_configured": "K\u00f6pr\u00fc zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor" + }, + "step": { + "manual_input": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + } + }, + "user": { + "data": { + "host": "Ke\u015ffedilen deCONZ a\u011f ge\u00e7idini se\u00e7in" + } + } + } + }, + "device_automation": { + "trigger_subtype": { + "side_4": "Yan 4", + "side_5": "Yan 5", + "side_6": "Yan 6" + }, + "trigger_type": { + "remote_awakened": "Cihaz uyand\u0131", + "remote_double_tap": "\" {subtype} \" cihaz\u0131na iki kez hafif\u00e7e vuruldu", + "remote_double_tap_any_side": "Cihaz herhangi bir tarafta \u00e7ift dokundu", + "remote_falling": "Serbest d\u00fc\u015f\u00fc\u015fte cihaz", + "remote_flip_180_degrees": "Cihaz 180 derece d\u00f6nd\u00fcr\u00fcld\u00fc", + "remote_flip_90_degrees": "Cihaz 90 derece d\u00f6nd\u00fcr\u00fcld\u00fc", + "remote_moved": "Cihaz \" {subtype} \" yukar\u0131 ta\u015f\u0131nd\u0131", + "remote_moved_any_side": "Cihaz herhangi bir taraf\u0131 yukar\u0131 gelecek \u015fekilde ta\u015f\u0131nd\u0131", + "remote_rotate_from_side_1": "Cihaz, \"1. taraftan\" \" {subtype} \" e d\u00f6nd\u00fcr\u00fcld\u00fc", + "remote_rotate_from_side_2": "Cihaz, \"2. taraftan\" \" {subtype} \" e d\u00f6nd\u00fcr\u00fcld\u00fc", + "remote_rotate_from_side_3": "Cihaz \"3. taraftan\" \" {subtype} \" e d\u00f6nd\u00fcr\u00fcld\u00fc", + "remote_rotate_from_side_4": "Cihaz, \"4. taraf\" dan \" {subtype} \" e d\u00f6nd\u00fcr\u00fcld\u00fc", + "remote_rotate_from_side_5": "Cihaz, \"5. taraf\" dan \" {subtype} \" e d\u00f6nd\u00fcr\u00fcld\u00fc", + "remote_turned_clockwise": "Cihaz saat y\u00f6n\u00fcnde d\u00f6nd\u00fc", + "remote_turned_counter_clockwise": "Cihaz saat y\u00f6n\u00fcn\u00fcn tersine d\u00f6nd\u00fc" + } + }, "options": { "step": { "deconz_devices": { diff --git a/homeassistant/components/deconz/translations/uk.json b/homeassistant/components/deconz/translations/uk.json new file mode 100644 index 00000000000..b5de362a731 --- /dev/null +++ b/homeassistant/components/deconz/translations/uk.json @@ -0,0 +1,105 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0446\u044c\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u043e.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "no_bridges": "\u0428\u043b\u044e\u0437\u0438 deCONZ \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456.", + "no_hardware_available": "\u0420\u0430\u0434\u0456\u043e\u043e\u0431\u043b\u0430\u0434\u043d\u0430\u043d\u043d\u044f \u043d\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0434\u043e deCONZ.", + "not_deconz_bridge": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0454 \u0448\u043b\u044e\u0437\u043e\u043c deCONZ.", + "updated_instance": "\u0410\u0434\u0440\u0435\u0441\u0443 \u0445\u043e\u0441\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043e." + }, + "error": { + "no_key": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u043a\u043b\u044e\u0447 API." + }, + "flow_title": "\u0428\u043b\u044e\u0437 Zigbee deCONZ ({host})", + "step": { + "hassio_confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e deCONZ (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Hass.io \"{addon}\")?", + "title": "Zigbee \u0448\u043b\u044e\u0437 deCONZ (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Hass.io)" + }, + "link": { + "description": "\u0420\u043e\u0437\u0431\u043b\u043e\u043a\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0454\u0441\u0442\u0440\u0430\u0446\u0456\u0457 \u0432 Home Assistant: \n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0434\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u044c \u0441\u0438\u0441\u0442\u0435\u043c\u0438 deCONZ - > Gateway - > Advanced.\n2. \u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u043a\u043d\u043e\u043f\u043a\u0443 \u00abAuthenticate app\u00bb.", + "title": "\u0417\u0432'\u044f\u0437\u043e\u043a \u0437 deCONZ" + }, + "manual_input": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + } + }, + "user": { + "data": { + "host": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u0438\u0439 \u0448\u043b\u044e\u0437 deCONZ" + } + } + } + }, + "device_automation": { + "trigger_subtype": { + "both_buttons": "\u041e\u0431\u0438\u0434\u0432\u0456 \u043a\u043d\u043e\u043f\u043a\u0438", + "bottom_buttons": "\u041d\u0438\u0436\u043d\u0456 \u043a\u043d\u043e\u043f\u043a\u0438", + "button_1": "\u041f\u0435\u0440\u0448\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "button_2": "\u0414\u0440\u0443\u0433\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "button_3": "\u0422\u0440\u0435\u0442\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "close": "\u0417\u0430\u043a\u0440\u0438\u0432\u0430\u0454\u0442\u044c\u0441\u044f", + "dim_down": "\u0417\u043c\u0435\u043d\u0448\u0438\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c", + "dim_up": "\u0417\u0431\u0456\u043b\u044c\u0448\u0438\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c", + "left": "\u041b\u0456\u0432\u043e\u0440\u0443\u0447", + "open": "\u0412\u0456\u0434\u043a\u0440\u0438\u0432\u0430\u0454\u0442\u044c\u0441\u044f", + "right": "\u041f\u0440\u0430\u0432\u043e\u0440\u0443\u0447", + "side_1": "\u0413\u0440\u0430\u043d\u044c 1", + "side_2": "\u0413\u0440\u0430\u043d\u044c 2", + "side_3": "\u0413\u0440\u0430\u043d\u044c 3", + "side_4": "\u0413\u0440\u0430\u043d\u044c 4", + "side_5": "\u0413\u0440\u0430\u043d\u044c 5", + "side_6": "\u0413\u0440\u0430\u043d\u044c 6", + "top_buttons": "\u0412\u0435\u0440\u0445\u043d\u0456 \u043a\u043d\u043e\u043f\u043a\u0438", + "turn_off": "\u0412\u0438\u043c\u043a\u043d\u0443\u0442\u0438", + "turn_on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438" + }, + "trigger_type": { + "remote_awakened": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0440\u043e\u0437\u0431\u0443\u0434\u0438\u043b\u0438", + "remote_button_double_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0438", + "remote_button_long_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u0434\u043e\u0432\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430", + "remote_button_long_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0456\u0441\u043b\u044f \u0434\u043e\u0432\u0433\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f", + "remote_button_quadruple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0447\u043e\u0442\u0438\u0440\u0438 \u0440\u0430\u0437\u0438", + "remote_button_quintuple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u043f'\u044f\u0442\u044c \u0440\u0430\u0437\u0456\u0432", + "remote_button_rotated": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u0442\u0430", + "remote_button_rotated_fast": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u0442\u0430 \u0448\u0432\u0438\u0434\u043a\u043e", + "remote_button_rotation_stopped": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043f\u0440\u0438\u043f\u0438\u043d\u0438\u043b\u0430 \u043e\u0431\u0435\u0440\u0442\u0430\u043d\u043d\u044f", + "remote_button_short_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430", + "remote_button_short_release": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0456\u0441\u043b\u044f \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f", + "remote_button_triple_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0438", + "remote_double_tap": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c {subtype} \u043f\u043e\u0441\u0442\u0443\u043a\u0430\u043b\u0438 \u0434\u0432\u0456\u0447\u0456", + "remote_double_tap_any_side": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c \u043f\u043e\u0441\u0442\u0443\u043a\u0430\u043b\u0438 \u0434\u0432\u0456\u0447\u0456", + "remote_falling": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0443 \u0432\u0456\u043b\u044c\u043d\u043e\u043c\u0443 \u043f\u0430\u0434\u0456\u043d\u043d\u0456", + "remote_flip_180_degrees": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u043d\u0430 180 \u0433\u0440\u0430\u0434\u0443\u0441\u0456\u0432", + "remote_flip_90_degrees": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u043d\u0430 90 \u0433\u0440\u0430\u0434\u0443\u0441\u0456\u0432", + "remote_gyro_activated": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u043e\u0442\u0440\u044f\u0441\u043b\u0438", + "remote_moved": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0437\u0440\u0443\u0448\u0438\u043b\u0438, \u043a\u043e\u043b\u0438 {subtype} \u0437\u0432\u0435\u0440\u0445\u0443", + "remote_moved_any_side": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0435\u0440\u0435\u043c\u0456\u0441\u0442\u0438\u043b\u0438", + "remote_rotate_from_side_1": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0437 \u0413\u0440\u0430\u043d\u0456 1 \u043d\u0430 {subtype}", + "remote_rotate_from_side_2": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0437 \u0413\u0440\u0430\u043d\u0456 2 \u043d\u0430 {subtype}", + "remote_rotate_from_side_3": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0437 \u0413\u0440\u0430\u043d\u0456 3 \u043d\u0430 {subtype}", + "remote_rotate_from_side_4": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0437 \u0413\u0440\u0430\u043d\u0456 4 \u043d\u0430 {subtype}", + "remote_rotate_from_side_5": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0437 \u0413\u0440\u0430\u043d\u0456 5 \u043d\u0430 {subtype}", + "remote_rotate_from_side_6": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0437 \u0413\u0440\u0430\u043d\u0456 6 \u043d\u0430 {subtype}", + "remote_turned_clockwise": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u0437\u0430 \u0433\u043e\u0434\u0438\u043d\u043d\u0438\u043a\u043e\u0432\u043e\u044e \u0441\u0442\u0440\u0456\u043b\u043a\u043e\u044e", + "remote_turned_counter_clockwise": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u043b\u0438 \u043f\u0440\u043e\u0442\u0438 \u0433\u043e\u0434\u0438\u043d\u043d\u0438\u043a\u043e\u0432\u043e\u0457 \u0441\u0442\u0440\u0456\u043b\u043a\u0438" + } + }, + "options": { + "step": { + "deconz_devices": { + "data": { + "allow_clip_sensor": "\u0412\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0438 deCONZ CLIP", + "allow_deconz_groups": "\u0412\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u0438 \u0433\u0440\u0443\u043f\u0438 \u043e\u0441\u0432\u0456\u0442\u043b\u0435\u043d\u043d\u044f deCONZ", + "allow_new_devices": "\u0414\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0435 \u0434\u043e\u0434\u0430\u0432\u0430\u043d\u043d\u044f \u043d\u043e\u0432\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u0456 \u0442\u0438\u043f\u0456\u0432 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 deCONZ", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f deCONZ" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/default_config/manifest.json b/homeassistant/components/default_config/manifest.json index 9a533092b8b..f8be3c9fe2a 100644 --- a/homeassistant/components/default_config/manifest.json +++ b/homeassistant/components/default_config/manifest.json @@ -6,6 +6,7 @@ "automation", "cloud", "counter", + "dhcp", "frontend", "history", "input_boolean", diff --git a/homeassistant/components/demo/cover.py b/homeassistant/components/demo/cover.py index e65d6e59ece..35f25df5a96 100644 --- a/homeassistant/components/demo/cover.py +++ b/homeassistant/components/demo/cover.py @@ -3,7 +3,11 @@ from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, SUPPORT_CLOSE, + SUPPORT_CLOSE_TILT, SUPPORT_OPEN, + SUPPORT_OPEN_TILT, + SUPPORT_SET_TILT_POSITION, + SUPPORT_STOP_TILT, CoverEntity, ) from homeassistant.core import callback @@ -26,6 +30,18 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= device_class="garage", supported_features=(SUPPORT_OPEN | SUPPORT_CLOSE), ), + DemoCover( + hass, + "cover_5", + "Pergola Roof", + tilt_position=60, + supported_features=( + SUPPORT_OPEN_TILT + | SUPPORT_STOP_TILT + | SUPPORT_CLOSE_TILT + | SUPPORT_SET_TILT_POSITION + ), + ), ] ) diff --git a/homeassistant/components/demo/translations/tr.json b/homeassistant/components/demo/translations/tr.json new file mode 100644 index 00000000000..1ca389b0b97 --- /dev/null +++ b/homeassistant/components/demo/translations/tr.json @@ -0,0 +1,12 @@ +{ + "options": { + "step": { + "options_1": { + "data": { + "constant": "Sabit" + } + } + } + }, + "title": "Demo" +} \ No newline at end of file diff --git a/homeassistant/components/demo/translations/uk.json b/homeassistant/components/demo/translations/uk.json new file mode 100644 index 00000000000..5ac1ac74708 --- /dev/null +++ b/homeassistant/components/demo/translations/uk.json @@ -0,0 +1,21 @@ +{ + "options": { + "step": { + "options_1": { + "data": { + "bool": "\u041b\u043e\u0433\u0456\u0447\u043d\u0438\u0439", + "constant": "\u041f\u043e\u0441\u0442\u0456\u0439\u043d\u0430", + "int": "\u0427\u0438\u0441\u043b\u043e\u0432\u0438\u0439" + } + }, + "options_2": { + "data": { + "multi": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0434\u0435\u043a\u0456\u043b\u044c\u043a\u0430", + "select": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u043f\u0446\u0456\u044e", + "string": "\u0421\u0442\u0440\u043e\u043a\u043e\u0432\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f" + } + } + } + }, + "title": "\u0414\u0435\u043c\u043e" +} \ No newline at end of file diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index 44cbd69bcd2..8d2052181f8 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -3,7 +3,7 @@ "name": "Denon AVR Network Receivers", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/denonavr", - "requirements": ["denonavr==0.9.9", "getmac==0.8.2"], + "requirements": ["denonavr==0.9.10", "getmac==0.8.2"], "codeowners": ["@scarface-4711", "@starkillerOG"], "ssdp": [ { diff --git a/homeassistant/components/denonavr/translations/de.json b/homeassistant/components/denonavr/translations/de.json index 5af7d3393e2..f52e6303091 100644 --- a/homeassistant/components/denonavr/translations/de.json +++ b/homeassistant/components/denonavr/translations/de.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt" + }, "step": { "select": { "data": { diff --git a/homeassistant/components/denonavr/translations/tr.json b/homeassistant/components/denonavr/translations/tr.json new file mode 100644 index 00000000000..f618d3a3038 --- /dev/null +++ b/homeassistant/components/denonavr/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "cannot_connect": "Ba\u011flan\u0131lamad\u0131, l\u00fctfen tekrar deneyin, ana g\u00fc\u00e7 ve ethernet kablolar\u0131n\u0131n ba\u011flant\u0131s\u0131n\u0131 kesip yeniden ba\u011flamak yard\u0131mc\u0131 olabilir" + }, + "step": { + "user": { + "data": { + "host": "\u0130p Adresi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/denonavr/translations/uk.json b/homeassistant/components/denonavr/translations/uk.json new file mode 100644 index 00000000000..efb4cb41777 --- /dev/null +++ b/homeassistant/components/denonavr/translations/uk.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "cannot_connect": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437. \u042f\u043a\u0449\u043e \u0446\u0435 \u043d\u0435 \u0441\u043f\u0440\u0430\u0446\u044e\u0432\u0430\u043b\u043e, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u043a\u0430\u0431\u0435\u043b\u044c Ethernet \u0456 \u043a\u0430\u0431\u0435\u043b\u044c \u0436\u0438\u0432\u043b\u0435\u043d\u043d\u044f.", + "not_denonavr_manufacturer": "\u0426\u0435 \u043d\u0435 \u0440\u0435\u0441\u0438\u0432\u0435\u0440 Denon. \u0412\u0438\u0440\u043e\u0431\u043d\u0438\u043a \u043d\u0435 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u0430\u0454.", + "not_denonavr_missing": "\u041d\u0435\u043f\u043e\u0432\u043d\u0430 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044f \u0434\u043b\u044f \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e." + }, + "error": { + "discovery_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u043d\u0430\u0439\u0442\u0438 \u0440\u0435\u0441\u0438\u0432\u0435\u0440 Denon." + }, + "flow_title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440 Denon: {name}", + "step": { + "confirm": { + "description": "\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0456\u0442\u044c \u0434\u043e\u0434\u0430\u0432\u0430\u043d\u043d\u044f \u0440\u0435\u0441\u0438\u0432\u0435\u0440\u0430", + "title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440 Denon" + }, + "select": { + "data": { + "select_host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430" + }, + "description": "\u041f\u043e\u0447\u043d\u0456\u0442\u044c \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u043d\u043e\u0432\u0443, \u044f\u043a\u0449\u043e \u0432\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 \u0456\u043d\u0448\u0438\u0439 \u0440\u0435\u0441\u0438\u0432\u0435\u0440", + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0440\u0435\u0441\u0438\u0432\u0435\u0440, \u044f\u043a\u0438\u0439 \u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438" + }, + "user": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430" + }, + "description": "\u042f\u043a\u0449\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u0432\u043a\u0430\u0437\u0430\u043d\u0430, \u0431\u0443\u0434\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0435 \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f", + "title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440 Denon" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_all_sources": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438 \u0432\u0441\u0456 \u0434\u0436\u0435\u0440\u0435\u043b\u0430", + "zone2": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u043e\u043d\u0438 2", + "zone3": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u043e\u043d\u0438 3" + }, + "description": "\u0414\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f", + "title": "\u0420\u0435\u0441\u0438\u0432\u0435\u0440 Denon" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/device_tracker/config_entry.py b/homeassistant/components/device_tracker/config_entry.py index 1be47b9b981..16e7d022c92 100644 --- a/homeassistant/components/device_tracker/config_entry.py +++ b/homeassistant/components/device_tracker/config_entry.py @@ -13,7 +13,7 @@ from homeassistant.const import ( from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_component import EntityComponent -from .const import ATTR_SOURCE_TYPE, DOMAIN, LOGGER +from .const import ATTR_HOST_NAME, ATTR_IP, ATTR_MAC, ATTR_SOURCE_TYPE, DOMAIN, LOGGER async def async_setup_entry(hass, entry): @@ -130,6 +130,21 @@ class TrackerEntity(BaseTrackerEntity): class ScannerEntity(BaseTrackerEntity): """Represent a tracked device that is on a scanned network.""" + @property + def ip_address(self) -> str: + """Return the primary ip address of the device.""" + return None + + @property + def mac_address(self) -> str: + """Return the mac address of the device.""" + return None + + @property + def hostname(self) -> str: + """Return hostname of the device.""" + return None + @property def state(self): """Return the state of the device.""" @@ -141,3 +156,17 @@ class ScannerEntity(BaseTrackerEntity): def is_connected(self): """Return true if the device is connected to the network.""" raise NotImplementedError + + @property + def state_attributes(self): + """Return the device state attributes.""" + attr = {} + attr.update(super().state_attributes) + if self.ip_address is not None: + attr[ATTR_IP] = self.ip_address + if self.mac_address is not None: + attr[ATTR_MAC] = self.mac_address + if self.hostname is not None: + attr[ATTR_HOST_NAME] = self.hostname + + return attr diff --git a/homeassistant/components/device_tracker/const.py b/homeassistant/components/device_tracker/const.py index c9ce9f2024a..aa1b349ef12 100644 --- a/homeassistant/components/device_tracker/const.py +++ b/homeassistant/components/device_tracker/const.py @@ -34,3 +34,4 @@ ATTR_LOCATION_NAME = "location_name" ATTR_MAC = "mac" ATTR_SOURCE_TYPE = "source_type" ATTR_CONSIDER_HOME = "consider_home" +ATTR_IP = "ip" diff --git a/homeassistant/components/device_tracker/translations/de.json b/homeassistant/components/device_tracker/translations/de.json index 651805dcb14..fe59183e67a 100644 --- a/homeassistant/components/device_tracker/translations/de.json +++ b/homeassistant/components/device_tracker/translations/de.json @@ -1,8 +1,12 @@ { "device_automation": { "condition_type": { - "is_home": "{entity_name} ist Zuhause", - "is_not_home": "{entity_name} ist nicht zu Hause" + "is_home": "{entity_name} ist zuhause", + "is_not_home": "{entity_name} ist nicht zuhause" + }, + "trigger_type": { + "enters": "{entity_name} betritt einen Bereich", + "leaves": "{entity_name} verl\u00e4sst einen Bereich" } }, "state": { diff --git a/homeassistant/components/device_tracker/translations/uk.json b/homeassistant/components/device_tracker/translations/uk.json index f49c7acc0e3..87945d2a19a 100644 --- a/homeassistant/components/device_tracker/translations/uk.json +++ b/homeassistant/components/device_tracker/translations/uk.json @@ -1,8 +1,18 @@ { + "device_automation": { + "condition_type": { + "is_home": "{entity_name} \u0432\u0434\u043e\u043c\u0430", + "is_not_home": "{entity_name} \u043d\u0435 \u0432\u0434\u043e\u043c\u0430" + }, + "trigger_type": { + "enters": "{entity_name} \u0432\u0445\u043e\u0434\u0438\u0442\u044c \u0432 \u0437\u043e\u043d\u0443", + "leaves": "{entity_name} \u043f\u043e\u043a\u0438\u0434\u0430\u0454 \u0437\u043e\u043d\u0443" + } + }, "state": { "_": { "home": "\u0412\u0434\u043e\u043c\u0430", - "not_home": "\u0412\u0456\u0434\u0441\u0443\u0442\u043d\u0456\u0439" + "not_home": "\u041d\u0435 \u0432\u0434\u043e\u043c\u0430" } }, "title": "\u0422\u0440\u0435\u043a\u0435\u0440 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" diff --git a/homeassistant/components/devolo_home_control/translations/de.json b/homeassistant/components/devolo_home_control/translations/de.json index 112daf582b3..6cf7ed3c821 100644 --- a/homeassistant/components/devolo_home_control/translations/de.json +++ b/homeassistant/components/devolo_home_control/translations/de.json @@ -1,7 +1,10 @@ { "config": { "abort": { - "already_configured": "Diese Home Control Zentral wird bereits verwendet." + "already_configured": "Konto wurde bereits konfiguriert" + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { "user": { @@ -9,7 +12,7 @@ "home_control_url": "Home Control URL", "mydevolo_url": "mydevolo URL", "password": "Passwort", - "username": "E-Mail-Adresse / devolo ID" + "username": "E-Mail / devolo ID" } } } diff --git a/homeassistant/components/devolo_home_control/translations/tr.json b/homeassistant/components/devolo_home_control/translations/tr.json new file mode 100644 index 00000000000..4c6b158f694 --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "E-posta / devolo ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/devolo_home_control/translations/uk.json b/homeassistant/components/devolo_home_control/translations/uk.json new file mode 100644 index 00000000000..d230d1918f5 --- /dev/null +++ b/homeassistant/components/devolo_home_control/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "step": { + "user": { + "data": { + "home_control_url": "Home Control URL-\u0430\u0434\u0440\u0435\u0441\u0430", + "mydevolo_url": "mydevolo URL-\u0430\u0434\u0440\u0435\u0441\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438 / devolo ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/de.json b/homeassistant/components/dexcom/translations/de.json index fadb459a3d3..d567dd6b611 100644 --- a/homeassistant/components/dexcom/translations/de.json +++ b/homeassistant/components/dexcom/translations/de.json @@ -4,7 +4,8 @@ "already_configured": "Konto ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/dexcom/translations/fr.json b/homeassistant/components/dexcom/translations/fr.json index d10643a3c1e..095c769a1be 100644 --- a/homeassistant/components/dexcom/translations/fr.json +++ b/homeassistant/components/dexcom/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9" + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" }, "error": { "cannot_connect": "\u00c9chec de connexion", diff --git a/homeassistant/components/dexcom/translations/tr.json b/homeassistant/components/dexcom/translations/tr.json index 80638d181b2..ec93dc078af 100644 --- a/homeassistant/components/dexcom/translations/tr.json +++ b/homeassistant/components/dexcom/translations/tr.json @@ -2,6 +2,28 @@ "config": { "abort": { "already_configured": "Hesap zaten konfig\u00fcre edilmi\u015fi durumda" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "\u00d6l\u00e7\u00fc birimi" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/dexcom/translations/uk.json b/homeassistant/components/dexcom/translations/uk.json new file mode 100644 index 00000000000..66727af90d1 --- /dev/null +++ b/homeassistant/components/dexcom/translations/uk.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "server": "\u0421\u0435\u0440\u0432\u0435\u0440", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0412\u0430\u0448\u0456 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0456 \u0434\u0430\u043d\u0456.", + "title": "Dexcom" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "unit_of_measurement": "\u041e\u0434\u0438\u043d\u0438\u0446\u044f \u0432\u0438\u043c\u0456\u0440\u0443" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dhcp/__init__.py b/homeassistant/components/dhcp/__init__.py new file mode 100644 index 00000000000..a71db430da4 --- /dev/null +++ b/homeassistant/components/dhcp/__init__.py @@ -0,0 +1,285 @@ +"""The dhcp integration.""" + +from abc import abstractmethod +import fnmatch +from ipaddress import ip_address as make_ip_address +import logging +import os +import threading + +from scapy.config import conf +from scapy.error import Scapy_Exception +from scapy.layers.dhcp import DHCP +from scapy.layers.l2 import Ether +from scapy.sendrecv import AsyncSniffer + +from homeassistant.components.device_tracker.const import ( + ATTR_HOST_NAME, + ATTR_IP, + ATTR_MAC, + ATTR_SOURCE_TYPE, + DOMAIN as DEVICE_TRACKER_DOMAIN, + SOURCE_TYPE_ROUTER, +) +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STARTED, + EVENT_HOMEASSISTANT_STOP, + STATE_HOME, +) +from homeassistant.core import Event, HomeAssistant, State, callback +from homeassistant.helpers.device_registry import format_mac +from homeassistant.helpers.event import async_track_state_added_domain +from homeassistant.loader import async_get_dhcp +from homeassistant.util.network import is_link_local + +from .const import DOMAIN + +FILTER = "udp and (port 67 or 68)" +REQUESTED_ADDR = "requested_addr" +MESSAGE_TYPE = "message-type" +HOSTNAME = "hostname" +MAC_ADDRESS = "macaddress" +IP_ADDRESS = "ip" +DHCP_REQUEST = 3 + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: dict) -> bool: + """Set up the dhcp component.""" + + async def _initialize(_): + address_data = {} + integration_matchers = await async_get_dhcp(hass) + watchers = [] + + for cls in (DHCPWatcher, DeviceTrackerWatcher): + watcher = cls(hass, address_data, integration_matchers) + await watcher.async_start() + watchers.append(watcher) + + async def _async_stop(*_): + for watcher in watchers: + await watcher.async_stop() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_stop) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _initialize) + return True + + +class WatcherBase: + """Base class for dhcp and device tracker watching.""" + + def __init__(self, hass, address_data, integration_matchers): + """Initialize class.""" + super().__init__() + + self.hass = hass + self._integration_matchers = integration_matchers + self._address_data = address_data + + def process_client(self, ip_address, hostname, mac_address): + """Process a client.""" + if is_link_local(make_ip_address(ip_address)): + # Ignore self assigned addresses + return + + data = self._address_data.get(ip_address) + + if data and data[MAC_ADDRESS] == mac_address and data[HOSTNAME] == hostname: + # If the address data is the same no need + # to process it + return + + self._address_data[ip_address] = {MAC_ADDRESS: mac_address, HOSTNAME: hostname} + + self.process_updated_address_data(ip_address, self._address_data[ip_address]) + + def process_updated_address_data(self, ip_address, data): + """Process the address data update.""" + lowercase_hostname = data[HOSTNAME].lower() + uppercase_mac = data[MAC_ADDRESS].upper() + + _LOGGER.debug( + "Processing updated address data for %s: mac=%s hostname=%s", + ip_address, + uppercase_mac, + lowercase_hostname, + ) + + for entry in self._integration_matchers: + if MAC_ADDRESS in entry and not fnmatch.fnmatch( + uppercase_mac, entry[MAC_ADDRESS] + ): + continue + + if HOSTNAME in entry and not fnmatch.fnmatch( + lowercase_hostname, entry[HOSTNAME] + ): + continue + + _LOGGER.debug("Matched %s against %s", data, entry) + + self.create_task( + self.hass.config_entries.flow.async_init( + entry["domain"], + context={"source": DOMAIN}, + data={IP_ADDRESS: ip_address, **data}, + ) + ) + + @abstractmethod + def create_task(self, task): + """Pass a task to async_add_task based on which context we are in.""" + + +class DeviceTrackerWatcher(WatcherBase): + """Class to watch dhcp data from routers.""" + + def __init__(self, hass, address_data, integration_matchers): + """Initialize class.""" + super().__init__(hass, address_data, integration_matchers) + self._unsub = None + + async def async_stop(self): + """Stop watching for new device trackers.""" + if self._unsub: + self._unsub() + self._unsub = None + + async def async_start(self): + """Stop watching for new device trackers.""" + self._unsub = async_track_state_added_domain( + self.hass, [DEVICE_TRACKER_DOMAIN], self._async_process_device_event + ) + for state in self.hass.states.async_all(DEVICE_TRACKER_DOMAIN): + self._async_process_device_state(state) + + @callback + def _async_process_device_event(self, event: Event): + """Process a device tracker state change event.""" + self._async_process_device_state(event.data.get("new_state")) + + @callback + def _async_process_device_state(self, state: State): + """Process a device tracker state.""" + if state.state != STATE_HOME: + return + + attributes = state.attributes + + if attributes.get(ATTR_SOURCE_TYPE) != SOURCE_TYPE_ROUTER: + return + + ip_address = attributes.get(ATTR_IP) + hostname = attributes.get(ATTR_HOST_NAME) + mac_address = attributes.get(ATTR_MAC) + + if ip_address is None or hostname is None or mac_address is None: + return + + self.process_client(ip_address, hostname, _format_mac(mac_address)) + + def create_task(self, task): + """Pass a task to async_create_task since we are in async context.""" + self.hass.async_create_task(task) + + +class DHCPWatcher(WatcherBase): + """Class to watch dhcp requests.""" + + def __init__(self, hass, address_data, integration_matchers): + """Initialize class.""" + super().__init__(hass, address_data, integration_matchers) + self._sniffer = None + self._started = threading.Event() + + async def async_stop(self): + """Stop watching for new device trackers.""" + await self.hass.async_add_executor_job(self._stop) + + def _stop(self): + """Stop the thread.""" + if self._started.is_set(): + self._sniffer.stop() + + async def async_start(self): + """Start watching for dhcp packets.""" + try: + _verify_l2socket_creation_permission() + except (Scapy_Exception, OSError) as ex: + if os.geteuid() == 0: + _LOGGER.error("Cannot watch for dhcp packets: %s", ex) + else: + _LOGGER.debug( + "Cannot watch for dhcp packets without root or CAP_NET_RAW: %s", ex + ) + return + + self._sniffer = AsyncSniffer( + filter=FILTER, + started_callback=self._started.set, + prn=self.handle_dhcp_packet, + store=0, + ) + self._sniffer.start() + + def handle_dhcp_packet(self, packet): + """Process a dhcp packet.""" + if DHCP not in packet: + return + + options = packet[DHCP].options + + request_type = _decode_dhcp_option(options, MESSAGE_TYPE) + if request_type != DHCP_REQUEST: + # DHCP request + return + + ip_address = _decode_dhcp_option(options, REQUESTED_ADDR) + hostname = _decode_dhcp_option(options, HOSTNAME) + mac_address = _format_mac(packet[Ether].src) + + if ip_address is None or hostname is None or mac_address is None: + return + + self.process_client(ip_address, hostname, mac_address) + + def create_task(self, task): + """Pass a task to hass.add_job since we are in a thread.""" + self.hass.add_job(task) + + +def _decode_dhcp_option(dhcp_options, key): + """Extract and decode data from a packet option.""" + for option in dhcp_options: + if len(option) < 2 or option[0] != key: + continue + + value = option[1] + if value is None or key != HOSTNAME: + return value + + # hostname is unicode + try: + return value.decode() + except (AttributeError, UnicodeDecodeError): + return None + + +def _format_mac(mac_address): + """Format a mac address for matching.""" + return format_mac(mac_address).replace(":", "") + + +def _verify_l2socket_creation_permission(): + """Create a socket using the scapy configured l2socket. + + Try to create the socket + to see if we have permissions + since AsyncSniffer will do it another + thread so we will not be able to capture + any permission or bind errors. + """ + conf.L2socket() diff --git a/homeassistant/components/dhcp/const.py b/homeassistant/components/dhcp/const.py new file mode 100644 index 00000000000..c28a699c64c --- /dev/null +++ b/homeassistant/components/dhcp/const.py @@ -0,0 +1,3 @@ +"""Constants for the dhcp integration.""" + +DOMAIN = "dhcp" diff --git a/homeassistant/components/dhcp/manifest.json b/homeassistant/components/dhcp/manifest.json new file mode 100644 index 00000000000..eda229ebec7 --- /dev/null +++ b/homeassistant/components/dhcp/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "dhcp", + "name": "DHCP Discovery", + "documentation": "https://www.home-assistant.io/integrations/dhcp", + "requirements": [ + "scapy==2.4.4" + ], + "codeowners": [ + "@bdraco" + ] +} diff --git a/homeassistant/components/dialogflow/translations/de.json b/homeassistant/components/dialogflow/translations/de.json index f1853107cc2..2035b818b44 100644 --- a/homeassistant/components/dialogflow/translations/de.json +++ b/homeassistant/components/dialogflow/translations/de.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", + "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." + }, "create_entry": { "default": "Um Ereignisse an den Home Assistant zu senden, musst du [Webhook-Integration von Dialogflow]({dialogflow_url}) einrichten. \n\nF\u00fclle die folgenden Informationen aus: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n - Inhaltstyp: application / json \n\nWeitere Informationen findest du in der [Dokumentation]({docs_url})." }, diff --git a/homeassistant/components/dialogflow/translations/tr.json b/homeassistant/components/dialogflow/translations/tr.json new file mode 100644 index 00000000000..84adcdf8225 --- /dev/null +++ b/homeassistant/components/dialogflow/translations/tr.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dialogflow/translations/uk.json b/homeassistant/components/dialogflow/translations/uk.json new file mode 100644 index 00000000000..625d2db78dc --- /dev/null +++ b/homeassistant/components/dialogflow/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "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.", + "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c." + }, + "create_entry": { + "default": "\u0414\u043b\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043a\u0438 \u043f\u043e\u0434\u0456\u0439 \u0432 Home Assistant \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Webhook \u0434\u043b\u044f [Dialogflow]({dialogflow_url}). \n\n\u0414\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e: \n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json \n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457." + }, + "step": { + "user": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Dialogflow?", + "title": "Dialogflow" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/translations/tr.json b/homeassistant/components/directv/translations/tr.json new file mode 100644 index 00000000000..daca8f1ef62 --- /dev/null +++ b/homeassistant/components/directv/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "unknown": "Beklenmeyen hata" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "ssdp_confirm": { + "description": "{name} kurmak istiyor musunuz?" + }, + "user": { + "data": { + "host": "Ana Bilgisayar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/translations/uk.json b/homeassistant/components/directv/translations/uk.json new file mode 100644 index 00000000000..5371f638e3d --- /dev/null +++ b/homeassistant/components/directv/translations/uk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "flow_title": "DirecTV: {name}", + "step": { + "ssdp_confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name}?" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/discord/manifest.json b/homeassistant/components/discord/manifest.json index 88bebe509b7..474705913c0 100644 --- a/homeassistant/components/discord/manifest.json +++ b/homeassistant/components/discord/manifest.json @@ -2,6 +2,6 @@ "domain": "discord", "name": "Discord", "documentation": "https://www.home-assistant.io/integrations/discord", - "requirements": ["discord.py==1.5.1"], + "requirements": ["discord.py==1.6.0"], "codeowners": [] } diff --git a/homeassistant/components/discord/notify.py b/homeassistant/components/discord/notify.py index b7fd193afad..dfc89a4cb7e 100644 --- a/homeassistant/components/discord/notify.py +++ b/homeassistant/components/discord/notify.py @@ -16,10 +16,15 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_TOKEN): cv.string}) - +ATTR_EMBED = "embed" +ATTR_EMBED_AUTHOR = "author" +ATTR_EMBED_FIELDS = "fields" +ATTR_EMBED_FOOTER = "footer" +ATTR_EMBED_THUMBNAIL = "thumbnail" ATTR_IMAGES = "images" +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_TOKEN): cv.string}) + def get_service(hass, config, discovery_info=None): """Get the Discord notification service.""" @@ -43,16 +48,21 @@ class DiscordNotificationService(BaseNotificationService): async def async_send_message(self, message, **kwargs): """Login to Discord, send message to channel(s) and log out.""" - discord.VoiceClient.warn_nacl = False discord_bot = discord.Client() images = None + embedding = None if ATTR_TARGET not in kwargs: _LOGGER.error("No target specified") return None + data = kwargs.get(ATTR_DATA) or {} + if ATTR_EMBED in data: + embedding = data[ATTR_EMBED] + fields = embedding.get(ATTR_EMBED_FIELDS) + if ATTR_IMAGES in data: images = [] @@ -86,7 +96,20 @@ class DiscordNotificationService(BaseNotificationService): files = [] for image in images: files.append(discord.File(image)) - await channel.send(message, files=files) + if embedding: + embed = discord.Embed(**embedding) + if fields: + for field_num, field_name in enumerate(fields): + embed.add_field(**fields[field_num]) + if ATTR_EMBED_FOOTER in embedding: + embed.set_footer(**embedding[ATTR_EMBED_FOOTER]) + if ATTR_EMBED_AUTHOR in embedding: + embed.set_author(**embedding[ATTR_EMBED_AUTHOR]) + if ATTR_EMBED_THUMBNAIL in embedding: + embed.set_thumbnail(**embedding[ATTR_EMBED_THUMBNAIL]) + await channel.send(message, files=files, embed=embed) + else: + await channel.send(message, files=files) except (discord.errors.HTTPException, discord.errors.NotFound) as error: _LOGGER.warning("Communication error: %s", error) await discord_bot.logout() diff --git a/homeassistant/components/dlna_dmr/media_player.py b/homeassistant/components/dlna_dmr/media_player.py index 2b01ad2a4ae..f8af118caed 100644 --- a/homeassistant/components/dlna_dmr/media_player.py +++ b/homeassistant/components/dlna_dmr/media_player.py @@ -281,7 +281,9 @@ class DlnaDmrDevice(MediaPlayerEntity): @property def volume_level(self): """Volume level of the media player (0..1).""" - return self._device.volume_level + if self._device.has_volume_level: + return self._device.volume_level + return 0 @catch_request_errors() async def async_set_volume_level(self, volume): diff --git a/homeassistant/components/doods/manifest.json b/homeassistant/components/doods/manifest.json index 0ca3444f70b..ecbcd8563a7 100644 --- a/homeassistant/components/doods/manifest.json +++ b/homeassistant/components/doods/manifest.json @@ -2,6 +2,6 @@ "domain": "doods", "name": "DOODS - Dedicated Open Object Detection Service", "documentation": "https://www.home-assistant.io/integrations/doods", - "requirements": ["pydoods==1.0.2", "pillow==7.2.0"], + "requirements": ["pydoods==1.0.2", "pillow==8.1.0"], "codeowners": [] } diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 1f7e02e8569..1dc5bf56c86 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -58,11 +58,14 @@ DEVICE_SCHEMA = vol.Schema( ) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - {vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [DEVICE_SCHEMA])} - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + {vol.Required(CONF_DEVICES): vol.All(cv.ensure_list, [DEVICE_SCHEMA])} + ) + }, + ), extra=vol.ALLOW_EXTRA, ) diff --git a/homeassistant/components/doorbird/translations/de.json b/homeassistant/components/doorbird/translations/de.json index 62bb11d6a8c..0d6bef7a63f 100644 --- a/homeassistant/components/doorbird/translations/de.json +++ b/homeassistant/components/doorbird/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Dieser DoorBird ist bereits konfiguriert", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "link_local_address": "Lokale Linkadressen werden nicht unterst\u00fctzt", "not_doorbird_device": "Dieses Ger\u00e4t ist kein DoorBird" }, diff --git a/homeassistant/components/doorbird/translations/tr.json b/homeassistant/components/doorbird/translations/tr.json new file mode 100644 index 00000000000..d7a1ca8a93a --- /dev/null +++ b/homeassistant/components/doorbird/translations/tr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "name": "Cihaz ad\u0131", + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/translations/uk.json b/homeassistant/components/doorbird/translations/uk.json new file mode 100644 index 00000000000..07bbdfacafe --- /dev/null +++ b/homeassistant/components/doorbird/translations/uk.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "link_local_address": "\u041f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u0456 \u0430\u0434\u0440\u0435\u0441\u0438 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f.", + "not_doorbird_device": "\u0426\u0435 \u043d\u0435 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 DoorBird." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "flow_title": "DoorBird {name} ({host})", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e DoorBird" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "events": "\u0421\u043f\u0438\u0441\u043e\u043a \u043f\u043e\u0434\u0456\u0439 \u0447\u0435\u0440\u0435\u0437 \u043a\u043e\u043c\u0443." + }, + "description": "\u0414\u043e\u0434\u0430\u0439\u0442\u0435 \u0447\u0435\u0440\u0435\u0437 \u043a\u043e\u043c\u0443 \u043d\u0430\u0437\u0432\u0438 \u043f\u043e\u0434\u0456\u0439, \u044f\u043a\u0435 \u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0432\u0456\u0434\u0441\u043b\u0456\u0434\u043a\u043e\u0432\u0443\u0432\u0430\u0442\u0438. \u041f\u0456\u0441\u043b\u044f \u0446\u044c\u043e\u0433\u043e, \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a DoorBird, \u0449\u043e\u0431 \u043f\u0440\u0438\u0437\u043d\u0430\u0447\u0438\u0442\u0438 \u0457\u0445 \u0434\u043e \u043f\u0435\u0432\u043d\u043e\u0457 \u043f\u043e\u0434\u0456\u0457. \u041f\u0440\u0438\u043a\u043b\u0430\u0434: somebody_pressed_the_button, motion. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457: https://www.home-assistant.io/integrations/doorbird/#events." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/downloader/__init__.py b/homeassistant/components/downloader/__init__.py index 0c87f04e3ab..94617ce43aa 100644 --- a/homeassistant/components/downloader/__init__.py +++ b/homeassistant/components/downloader/__init__.py @@ -9,7 +9,7 @@ import voluptuous as vol from homeassistant.const import HTTP_OK import homeassistant.helpers.config_validation as cv -from homeassistant.util import sanitize_filename +from homeassistant.util import raise_if_invalid_filename, raise_if_invalid_path _LOGGER = logging.getLogger(__name__) @@ -70,8 +70,8 @@ def setup(hass, config): overwrite = service.data.get(ATTR_OVERWRITE) - if subdir: - subdir = sanitize_filename(subdir) + # Check the path + raise_if_invalid_path(subdir) final_path = None @@ -101,8 +101,8 @@ def setup(hass, config): if not filename: filename = "ha_download" - # Remove stuff to ruin paths - filename = sanitize_filename(filename) + # Check the filename + raise_if_invalid_filename(filename) # Do we want to download to subdir, create if needed if subdir: @@ -148,6 +148,16 @@ def setup(hass, config): {"url": url, "filename": filename}, ) + # Remove file if we started downloading but failed + if final_path and os.path.isfile(final_path): + os.remove(final_path) + except ValueError: + _LOGGER.exception("Invalid value") + hass.bus.fire( + f"{DOMAIN}_{DOWNLOAD_FAILED_EVENT}", + {"url": url, "filename": filename}, + ) + # Remove file if we started downloading but failed if final_path and os.path.isfile(final_path): os.remove(final_path) diff --git a/homeassistant/components/dsmr/translations/de.json b/homeassistant/components/dsmr/translations/de.json new file mode 100644 index 00000000000..da1d200c2a2 --- /dev/null +++ b/homeassistant/components/dsmr/translations/de.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/fr.json b/homeassistant/components/dsmr/translations/fr.json index ea382532a71..cb08a7865b3 100644 --- a/homeassistant/components/dsmr/translations/fr.json +++ b/homeassistant/components/dsmr/translations/fr.json @@ -7,5 +7,15 @@ "one": "", "other": "Autre" } + }, + "options": { + "step": { + "init": { + "data": { + "time_between_update": "Temps minimum entre les mises \u00e0 jour des entit\u00e9s" + }, + "title": "Options DSMR" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/dsmr/translations/tr.json b/homeassistant/components/dsmr/translations/tr.json index 94c31d0e156..0857160dc51 100644 --- a/homeassistant/components/dsmr/translations/tr.json +++ b/homeassistant/components/dsmr/translations/tr.json @@ -1,4 +1,9 @@ { + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/dsmr/translations/uk.json b/homeassistant/components/dsmr/translations/uk.json new file mode 100644 index 00000000000..9bca6b00c74 --- /dev/null +++ b/homeassistant/components/dsmr/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + } + }, + "options": { + "step": { + "init": { + "data": { + "time_between_update": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + }, + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 DSMR" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dunehd/translations/tr.json b/homeassistant/components/dunehd/translations/tr.json new file mode 100644 index 00000000000..0f8c17228fd --- /dev/null +++ b/homeassistant/components/dunehd/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar" + }, + "title": "Dune HD" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dunehd/translations/uk.json b/homeassistant/components/dunehd/translations/uk.json new file mode 100644 index 00000000000..d2f4eadbdcb --- /dev/null +++ b/homeassistant/components/dunehd/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_host": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 Dune HD. \u042f\u043a\u0449\u043e \u0443 \u0412\u0430\u0441 \u0432\u0438\u043d\u0438\u043a\u043b\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0437 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u043c, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438 \u0437\u0430 \u0430\u0434\u0440\u0435\u0441\u043e\u044e: https://www.home-assistant.io/integrations/dunehd \n\n \u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0412\u0430\u0448 \u043f\u043b\u0435\u0454\u0440 \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u0438\u0439.", + "title": "Dune HD" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dyson/__init__.py b/homeassistant/components/dyson/__init__.py index fbe7897e6bb..b39af2a2fd1 100644 --- a/homeassistant/components/dyson/__init__.py +++ b/homeassistant/components/dyson/__init__.py @@ -7,6 +7,7 @@ import voluptuous as vol from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) @@ -108,3 +109,45 @@ def setup(hass, config): discovery.load_platform(hass, platform, DOMAIN, {}, config) return True + + +class DysonEntity(Entity): + """Representation of a Dyson entity.""" + + def __init__(self, device, state_type): + """Initialize the entity.""" + self._device = device + self._state_type = state_type + + async def async_added_to_hass(self): + """Call when entity is added to hass.""" + self._device.add_message_listener(self.on_message_filter) + + def on_message_filter(self, message): + """Filter new messages received.""" + if self._state_type is None or isinstance(message, self._state_type): + _LOGGER.debug( + "Message received for device %s : %s", + self.name, + message, + ) + self.on_message(message) + + def on_message(self, message): + """Handle new messages received.""" + self.schedule_update_ha_state() + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the name of the Dyson sensor.""" + return self._device.name + + @property + def unique_id(self): + """Return the sensor's unique id.""" + return self._device.serial diff --git a/homeassistant/components/dyson/air_quality.py b/homeassistant/components/dyson/air_quality.py index 4bc41e3b8fc..d23b2b1ef88 100644 --- a/homeassistant/components/dyson/air_quality.py +++ b/homeassistant/components/dyson/air_quality.py @@ -4,9 +4,9 @@ import logging from libpurecool.dyson_pure_cool import DysonPureCool from libpurecool.dyson_pure_state_v2 import DysonEnvironmentalSensorV2State -from homeassistant.components.air_quality import DOMAIN, AirQualityEntity +from homeassistant.components.air_quality import AirQualityEntity -from . import DYSON_DEVICES +from . import DYSON_DEVICES, DysonEntity ATTRIBUTION = "Dyson purifier air quality sensor" @@ -39,41 +39,23 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(hass.data[DYSON_AIQ_DEVICES]) -class DysonAirSensor(AirQualityEntity): +class DysonAirSensor(DysonEntity, AirQualityEntity): """Representation of a generic Dyson air quality sensor.""" def __init__(self, device): """Create a new generic air quality Dyson sensor.""" - self._device = device + super().__init__(device, DysonEnvironmentalSensorV2State) self._old_value = None - self._name = device.name - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self._device.add_message_listener(self.on_message) def on_message(self, message): """Handle new messages which are received from the fan.""" - _LOGGER.debug( - "%s: Message received for %s device: %s", DOMAIN, self.name, message - ) if ( self._old_value is None or self._old_value != self._device.environmental_state - ) and isinstance(message, DysonEnvironmentalSensorV2State): + ): self._old_value = self._device.environmental_state self.schedule_update_ha_state() - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the name of the Dyson sensor.""" - return self._name - @property def attribution(self): """Return the attribution.""" @@ -92,42 +74,24 @@ class DysonAirSensor(AirQualityEntity): @property def particulate_matter_2_5(self): """Return the particulate matter 2.5 level.""" - if self._device.environmental_state: - return int(self._device.environmental_state.particulate_matter_25) - return None + return int(self._device.environmental_state.particulate_matter_25) @property def particulate_matter_10(self): """Return the particulate matter 10 level.""" - if self._device.environmental_state: - return int(self._device.environmental_state.particulate_matter_10) - return None + return int(self._device.environmental_state.particulate_matter_10) @property def nitrogen_dioxide(self): """Return the NO2 (nitrogen dioxide) level.""" - if self._device.environmental_state: - return int(self._device.environmental_state.nitrogen_dioxide) - return None + return int(self._device.environmental_state.nitrogen_dioxide) @property def volatile_organic_compounds(self): """Return the VOC (Volatile Organic Compounds) level.""" - if self._device.environmental_state: - return int(self._device.environmental_state.volatile_organic_compounds) - return None - - @property - def unique_id(self): - """Return the sensor's unique id.""" - return self._device.serial + return int(self._device.environmental_state.volatile_organic_compounds) @property def device_state_attributes(self): """Return the device state attributes.""" - data = {} - - voc = self.volatile_organic_compounds - if voc is not None: - data[ATTR_VOC] = voc - return data + return {ATTR_VOC: self.volatile_organic_compounds} diff --git a/homeassistant/components/dyson/climate.py b/homeassistant/components/dyson/climate.py index a71c124c633..e7b8f42f1b1 100644 --- a/homeassistant/components/dyson/climate.py +++ b/homeassistant/components/dyson/climate.py @@ -37,13 +37,13 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from . import DYSON_DEVICES +from . import DYSON_DEVICES, DysonEntity _LOGGER = logging.getLogger(__name__) SUPPORT_FAN = [FAN_FOCUS, FAN_DIFFUSE] SUPPORT_FAN_PCOOL = [FAN_OFF, FAN_AUTO, FAN_LOW, FAN_MEDIUM, FAN_HIGH] -SUPPORT_HVAG = [HVAC_MODE_COOL, HVAC_MODE_HEAT] +SUPPORT_HVAC = [HVAC_MODE_COOL, HVAC_MODE_HEAT] SUPPORT_HVAC_PCOOL = [HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF] SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE @@ -88,41 +88,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(new_entities) -class DysonPureHotCoolLinkEntity(ClimateEntity): +class DysonClimateEntity(DysonEntity, ClimateEntity): """Representation of a Dyson climate fan.""" - def __init__(self, device): - """Initialize the fan.""" - self._device = device - self._current_temp = None - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self._device.add_message_listener(self.on_message) - - def on_message(self, message): - """Call when new messages received from the climate.""" - if isinstance(message, DysonPureHotCoolState): - _LOGGER.debug( - "Message received for climate device %s : %s", self.name, message - ) - self.schedule_update_ha_state() - - @property - def should_poll(self): - """No polling needed.""" - return False - @property def supported_features(self): """Return the list of supported features.""" return SUPPORT_FLAGS - @property - def name(self): - """Return the display name of this climate.""" - return self._device.name - @property def temperature_unit(self): """Return the unit of measurement.""" @@ -131,11 +104,13 @@ class DysonPureHotCoolLinkEntity(ClimateEntity): @property def current_temperature(self): """Return the current temperature.""" - if self._device.environmental_state: + if ( + self._device.environmental_state + and self._device.environmental_state.temperature + ): temperature_kelvin = self._device.environmental_state.temperature - if temperature_kelvin != 0: - self._current_temp = float(f"{(temperature_kelvin - 273):.1f}") - return self._current_temp + return float("{:.1f}".format(temperature_kelvin - 273)) + return None @property def target_temperature(self): @@ -146,12 +121,49 @@ class DysonPureHotCoolLinkEntity(ClimateEntity): @property def current_humidity(self): """Return the current humidity.""" - if self._device.environmental_state: - if self._device.environmental_state.humidity == 0: - return None + # Humidity equaling to 0 means invalid value so we don't check for None here + # https://github.com/home-assistant/core/pull/45172#discussion_r559069756 + if ( + self._device.environmental_state + and self._device.environmental_state.humidity + ): return self._device.environmental_state.humidity return None + @property + def min_temp(self): + """Return the minimum temperature.""" + return 1 + + @property + def max_temp(self): + """Return the maximum temperature.""" + return 37 + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + target_temp = kwargs.get(ATTR_TEMPERATURE) + if target_temp is None: + _LOGGER.error("Missing target temperature %s", kwargs) + return + target_temp = int(target_temp) + _LOGGER.debug("Set %s temperature %s", self.name, target_temp) + # Limit the target temperature into acceptable range. + target_temp = min(self.max_temp, target_temp) + target_temp = max(self.min_temp, target_temp) + self.set_heat_target(HeatTarget.celsius(target_temp)) + + def set_heat_target(self, heat_target): + """Set heating target temperature.""" + + +class DysonPureHotCoolLinkEntity(DysonClimateEntity): + """Representation of a Dyson climate fan.""" + + def __init__(self, device): + """Initialize the fan.""" + super().__init__(device, DysonPureHotCoolState) + @property def hvac_mode(self): """Return hvac operation ie. heat, cool mode. @@ -168,7 +180,7 @@ class DysonPureHotCoolLinkEntity(ClimateEntity): Need to be a subset of HVAC_MODES. """ - return SUPPORT_HVAG + return SUPPORT_HVAC @property def hvac_action(self): @@ -194,18 +206,10 @@ class DysonPureHotCoolLinkEntity(ClimateEntity): """Return the list of available fan modes.""" return SUPPORT_FAN - def set_temperature(self, **kwargs): - """Set new target temperature.""" - target_temp = kwargs.get(ATTR_TEMPERATURE) - if target_temp is None: - return - target_temp = int(target_temp) - _LOGGER.debug("Set %s temperature %s", self.name, target_temp) - # Limit the target temperature into acceptable range. - target_temp = min(self.max_temp, target_temp) - target_temp = max(self.min_temp, target_temp) + def set_heat_target(self, heat_target): + """Set heating target temperature.""" self._device.set_configuration( - heat_target=HeatTarget.celsius(target_temp), heat_mode=HeatMode.HEAT_ON + heat_target=heat_target, heat_mode=HeatMode.HEAT_ON ) def set_fan_mode(self, fan_mode): @@ -224,78 +228,13 @@ class DysonPureHotCoolLinkEntity(ClimateEntity): elif hvac_mode == HVAC_MODE_COOL: self._device.set_configuration(heat_mode=HeatMode.HEAT_OFF) - @property - def min_temp(self): - """Return the minimum temperature.""" - return 1 - @property - def max_temp(self): - """Return the maximum temperature.""" - return 37 - - -class DysonPureHotCoolEntity(ClimateEntity): +class DysonPureHotCoolEntity(DysonClimateEntity): """Representation of a Dyson climate hot+cool fan.""" def __init__(self, device): """Initialize the fan.""" - self._device = device - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self._device.add_message_listener(self.on_message) - - def on_message(self, message): - """Call when new messages received from the climate device.""" - if isinstance(message, DysonPureHotCoolV2State): - _LOGGER.debug( - "Message received for climate device %s : %s", self.name, message - ) - self.schedule_update_ha_state() - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def supported_features(self): - """Return the list of supported features.""" - return SUPPORT_FLAGS - - @property - def name(self): - """Return the display name of this climate.""" - return self._device.name - - @property - def temperature_unit(self): - """Return the unit of measurement.""" - return TEMP_CELSIUS - - @property - def current_temperature(self): - """Return the current temperature.""" - if self._device.environmental_state.temperature is not None: - temperature_kelvin = self._device.environmental_state.temperature - if temperature_kelvin != 0: - return float("{:.1f}".format(temperature_kelvin - 273)) - return None - - @property - def target_temperature(self): - """Return the target temperature.""" - heat_target = int(self._device.state.heat_target) / 10 - return int(heat_target - 273) - - @property - def current_humidity(self): - """Return the current humidity.""" - if self._device.environmental_state.humidity is not None: - if self._device.environmental_state.humidity != 0: - return self._device.environmental_state.humidity - return None + super().__init__(device, DysonPureHotCoolV2State) @property def hvac_mode(self): @@ -347,18 +286,9 @@ class DysonPureHotCoolEntity(ClimateEntity): """Return the list of available fan modes.""" return SUPPORT_FAN_PCOOL - def set_temperature(self, **kwargs): - """Set new target temperature.""" - target_temp = kwargs.get(ATTR_TEMPERATURE) - if target_temp is None: - _LOGGER.error("Missing target temperature %s", kwargs) - return - target_temp = int(target_temp) - _LOGGER.debug("Set %s temperature %s", self.name, target_temp) - # Limit the target temperature into acceptable range. - target_temp = min(self.max_temp, target_temp) - target_temp = max(self.min_temp, target_temp) - self._device.set_heat_target(HeatTarget.celsius(target_temp)) + def set_heat_target(self, heat_target): + """Set heating target temperature.""" + self._device.set_heat_target(heat_target) def set_fan_mode(self, fan_mode): """Set new fan mode.""" @@ -385,13 +315,3 @@ class DysonPureHotCoolEntity(ClimateEntity): self._device.enable_heat_mode() elif hvac_mode == HVAC_MODE_COOL: self._device.disable_heat_mode() - - @property - def min_temp(self): - """Return the minimum temperature.""" - return 1 - - @property - def max_temp(self): - """Return the maximum temperature.""" - return 37 diff --git a/homeassistant/components/dyson/fan.py b/homeassistant/components/dyson/fan.py index ca685f36a13..6690f77390d 100644 --- a/homeassistant/components/dyson/fan.py +++ b/homeassistant/components/dyson/fan.py @@ -1,5 +1,6 @@ """Support for Dyson Pure Cool link fan.""" import logging +from typing import Optional from libpurecool.const import FanMode, FanSpeed, NightMode, Oscillation from libpurecool.dyson_pure_cool import DysonPureCool @@ -16,10 +17,9 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, FanEntity, ) -from homeassistant.const import ATTR_ENTITY_ID -import homeassistant.helpers.config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_platform -from . import DYSON_DEVICES +from . import DYSON_DEVICES, DysonEntity _LOGGER = logging.getLogger(__name__) @@ -44,51 +44,69 @@ SERVICE_SET_FLOW_DIRECTION_FRONT = "set_flow_direction_front" SERVICE_SET_TIMER = "set_timer" SERVICE_SET_DYSON_SPEED = "set_speed" -DYSON_SET_NIGHT_MODE_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_id, - vol.Required(ATTR_NIGHT_MODE): cv.boolean, - } -) +SET_NIGHT_MODE_SCHEMA = { + vol.Required(ATTR_NIGHT_MODE): cv.boolean, +} -SET_AUTO_MODE_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_id, - vol.Required(ATTR_AUTO_MODE): cv.boolean, - } -) +SET_AUTO_MODE_SCHEMA = { + vol.Required(ATTR_AUTO_MODE): cv.boolean, +} -SET_ANGLE_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_id, - vol.Required(ATTR_ANGLE_LOW): cv.positive_int, - vol.Required(ATTR_ANGLE_HIGH): cv.positive_int, - } -) +SET_ANGLE_SCHEMA = { + vol.Required(ATTR_ANGLE_LOW): cv.positive_int, + vol.Required(ATTR_ANGLE_HIGH): cv.positive_int, +} -SET_FLOW_DIRECTION_FRONT_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_id, - vol.Required(ATTR_FLOW_DIRECTION_FRONT): cv.boolean, - } -) +SET_FLOW_DIRECTION_FRONT_SCHEMA = { + vol.Required(ATTR_FLOW_DIRECTION_FRONT): cv.boolean, +} -SET_TIMER_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_id, - vol.Required(ATTR_TIMER): cv.positive_int, - } -) +SET_TIMER_SCHEMA = { + vol.Required(ATTR_TIMER): cv.positive_int, +} -SET_DYSON_SPEED_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_id, - vol.Required(ATTR_DYSON_SPEED): cv.positive_int, - } -) +SET_DYSON_SPEED_SCHEMA = { + vol.Required(ATTR_DYSON_SPEED): cv.positive_int, +} -def setup_platform(hass, config, add_entities, discovery_info=None): +SPEED_LIST_HA = [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + +SPEED_LIST_DYSON = [ + int(FanSpeed.FAN_SPEED_1.value), + int(FanSpeed.FAN_SPEED_2.value), + int(FanSpeed.FAN_SPEED_3.value), + int(FanSpeed.FAN_SPEED_4.value), + int(FanSpeed.FAN_SPEED_5.value), + int(FanSpeed.FAN_SPEED_6.value), + int(FanSpeed.FAN_SPEED_7.value), + int(FanSpeed.FAN_SPEED_8.value), + int(FanSpeed.FAN_SPEED_9.value), + int(FanSpeed.FAN_SPEED_10.value), +] + +SPEED_DYSON_TO_HA = { + FanSpeed.FAN_SPEED_1.value: SPEED_LOW, + FanSpeed.FAN_SPEED_2.value: SPEED_LOW, + FanSpeed.FAN_SPEED_3.value: SPEED_LOW, + FanSpeed.FAN_SPEED_4.value: SPEED_LOW, + FanSpeed.FAN_SPEED_AUTO.value: SPEED_MEDIUM, + FanSpeed.FAN_SPEED_5.value: SPEED_MEDIUM, + FanSpeed.FAN_SPEED_6.value: SPEED_MEDIUM, + FanSpeed.FAN_SPEED_7.value: SPEED_MEDIUM, + FanSpeed.FAN_SPEED_8.value: SPEED_HIGH, + FanSpeed.FAN_SPEED_9.value: SPEED_HIGH, + FanSpeed.FAN_SPEED_10.value: SPEED_HIGH, +} + +SPEED_HA_TO_DYSON = { + SPEED_LOW: FanSpeed.FAN_SPEED_4, + SPEED_MEDIUM: FanSpeed.FAN_SPEED_7, + SPEED_HIGH: FanSpeed.FAN_SPEED_10, +} + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Dyson fan components.""" if discovery_info is None: @@ -105,131 +123,121 @@ def setup_platform(hass, config, add_entities, discovery_info=None): if device.serial not in device_serials: if isinstance(device, DysonPureCool): has_purecool_devices = True - dyson_entity = DysonPureCoolDevice(device) + dyson_entity = DysonPureCoolEntity(device) hass.data[DYSON_FAN_DEVICES].append(dyson_entity) elif isinstance(device, DysonPureCoolLink): - dyson_entity = DysonPureCoolLinkDevice(hass, device) + dyson_entity = DysonPureCoolLinkEntity(device) hass.data[DYSON_FAN_DEVICES].append(dyson_entity) - add_entities(hass.data[DYSON_FAN_DEVICES]) + async_add_entities(hass.data[DYSON_FAN_DEVICES]) - def service_handle(service): - """Handle the Dyson services.""" - entity_id = service.data[ATTR_ENTITY_ID] - fan_device = next( - (fan for fan in hass.data[DYSON_FAN_DEVICES] if fan.entity_id == entity_id), - None, - ) - if fan_device is None: - _LOGGER.warning("Unable to find Dyson fan device %s", str(entity_id)) - return - - if service.service == SERVICE_SET_NIGHT_MODE: - fan_device.set_night_mode(service.data[ATTR_NIGHT_MODE]) - - if service.service == SERVICE_SET_AUTO_MODE: - fan_device.set_auto_mode(service.data[ATTR_AUTO_MODE]) - - if service.service == SERVICE_SET_ANGLE: - fan_device.set_angle( - service.data[ATTR_ANGLE_LOW], service.data[ATTR_ANGLE_HIGH] - ) - - if service.service == SERVICE_SET_FLOW_DIRECTION_FRONT: - fan_device.set_flow_direction_front(service.data[ATTR_FLOW_DIRECTION_FRONT]) - - if service.service == SERVICE_SET_TIMER: - fan_device.set_timer(service.data[ATTR_TIMER]) - - if service.service == SERVICE_SET_DYSON_SPEED: - fan_device.set_dyson_speed(service.data[ATTR_DYSON_SPEED]) - - # Register dyson service(s) - hass.services.register( - DYSON_DOMAIN, - SERVICE_SET_NIGHT_MODE, - service_handle, - schema=DYSON_SET_NIGHT_MODE_SCHEMA, + # Register custom services + platform = entity_platform.current_platform.get() + platform.async_register_entity_service( + SERVICE_SET_NIGHT_MODE, SET_NIGHT_MODE_SCHEMA, "set_night_mode" ) - - hass.services.register( - DYSON_DOMAIN, SERVICE_SET_AUTO_MODE, service_handle, schema=SET_AUTO_MODE_SCHEMA + platform.async_register_entity_service( + SERVICE_SET_AUTO_MODE, SET_AUTO_MODE_SCHEMA, "set_auto_mode" + ) + platform.async_register_entity_service( + SERVICE_SET_DYSON_SPEED, SET_DYSON_SPEED_SCHEMA, "service_set_dyson_speed" ) if has_purecool_devices: - hass.services.register( - DYSON_DOMAIN, SERVICE_SET_ANGLE, service_handle, schema=SET_ANGLE_SCHEMA + platform.async_register_entity_service( + SERVICE_SET_ANGLE, SET_ANGLE_SCHEMA, "set_angle" ) - - hass.services.register( - DYSON_DOMAIN, + platform.async_register_entity_service( SERVICE_SET_FLOW_DIRECTION_FRONT, - service_handle, - schema=SET_FLOW_DIRECTION_FRONT_SCHEMA, + SET_FLOW_DIRECTION_FRONT_SCHEMA, + "set_flow_direction_front", ) - - hass.services.register( - DYSON_DOMAIN, SERVICE_SET_TIMER, service_handle, schema=SET_TIMER_SCHEMA - ) - - hass.services.register( - DYSON_DOMAIN, - SERVICE_SET_DYSON_SPEED, - service_handle, - schema=SET_DYSON_SPEED_SCHEMA, + platform.async_register_entity_service( + SERVICE_SET_TIMER, SET_TIMER_SCHEMA, "set_timer" ) -class DysonPureCoolLinkDevice(FanEntity): +class DysonFanEntity(DysonEntity, FanEntity): """Representation of a Dyson fan.""" - def __init__(self, hass, device): - """Initialize the fan.""" - _LOGGER.debug("Creating device %s", device.name) - self.hass = hass - self._device = device - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self._device.add_message_listener(self.on_message) - - def on_message(self, message): - """Call when new messages received from the fan.""" - - if isinstance(message, DysonPureCoolState): - _LOGGER.debug("Message received for fan device %s: %s", self.name, message) - self.schedule_update_ha_state() + @property + def speed(self): + """Return the current speed.""" + return SPEED_DYSON_TO_HA[self._device.state.speed] @property - def should_poll(self): - """No polling needed.""" - return False + def speed_list(self) -> list: + """Get the list of available speeds.""" + return SPEED_LIST_HA @property - def name(self): - """Return the display name of this fan.""" - return self._device.name + def dyson_speed(self): + """Return the current speed.""" + if self._device.state.speed == FanSpeed.FAN_SPEED_AUTO.value: + return self._device.state.speed + return int(self._device.state.speed) + + @property + def dyson_speed_list(self) -> list: + """Get the list of available dyson speeds.""" + return SPEED_LIST_DYSON + + @property + def night_mode(self): + """Return Night mode.""" + return self._device.state.night_mode == "ON" + + @property + def auto_mode(self): + """Return auto mode.""" + raise NotImplementedError + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return SUPPORT_OSCILLATE | SUPPORT_SET_SPEED + + @property + def device_state_attributes(self) -> dict: + """Return optional state attributes.""" + return { + ATTR_NIGHT_MODE: self.night_mode, + ATTR_AUTO_MODE: self.auto_mode, + ATTR_DYSON_SPEED: self.dyson_speed, + ATTR_DYSON_SPEED_LIST: self.dyson_speed_list, + } def set_speed(self, speed: str) -> None: - """Set the speed of the fan. Never called ??.""" + """Set the speed of the fan.""" + if speed not in SPEED_LIST_HA: + raise ValueError(f'"{speed}" is not a valid speed') _LOGGER.debug("Set fan speed to: %s", speed) + self.set_dyson_speed(SPEED_HA_TO_DYSON[speed]) - if speed == FanSpeed.FAN_SPEED_AUTO.value: - self._device.set_configuration(fan_mode=FanMode.AUTO) - else: - fan_speed = FanSpeed(f"{int(speed):04d}") - self._device.set_configuration(fan_mode=FanMode.FAN, fan_speed=fan_speed) + def set_dyson_speed(self, speed: FanSpeed) -> None: + """Set the exact speed of the fan.""" + raise NotImplementedError - def turn_on(self, speed: str = None, **kwargs) -> None: + def service_set_dyson_speed(self, dyson_speed: str) -> None: + """Handle the service to set dyson speed.""" + if dyson_speed not in SPEED_LIST_DYSON: + raise ValueError(f'"{dyson_speed}" is not a valid Dyson speed') + _LOGGER.debug("Set exact speed to %s", dyson_speed) + speed = FanSpeed(f"{int(dyson_speed):04d}") + self.set_dyson_speed(speed) + + +class DysonPureCoolLinkEntity(DysonFanEntity): + """Representation of a Dyson fan.""" + + def __init__(self, device): + """Initialize the fan.""" + super().__init__(device, DysonPureCoolState) + + def turn_on(self, speed: Optional[str] = None, **kwargs) -> None: """Turn on the fan.""" _LOGGER.debug("Turn on fan %s with speed %s", self.name, speed) - if speed: - if speed == FanSpeed.FAN_SPEED_AUTO.value: - self._device.set_configuration(fan_mode=FanMode.AUTO) - else: - fan_speed = FanSpeed(f"{int(speed):04d}") - self._device.set_configuration( - fan_mode=FanMode.FAN, fan_speed=fan_speed - ) + if speed is not None: + self.set_speed(speed) else: # Speed not set, just turn on self._device.set_configuration(fan_mode=FanMode.FAN) @@ -239,6 +247,10 @@ class DysonPureCoolLinkDevice(FanEntity): _LOGGER.debug("Turn off fan %s", self.name) self._device.set_configuration(fan_mode=FanMode.OFF) + def set_dyson_speed(self, speed: FanSpeed) -> None: + """Set the exact speed of the fan.""" + self._device.set_configuration(fan_mode=FanMode.FAN, fan_speed=speed) + def oscillate(self, oscillating: bool) -> None: """Turn on/off oscillating.""" _LOGGER.debug("Turn oscillation %s for device %s", oscillating, self.name) @@ -251,33 +263,12 @@ class DysonPureCoolLinkDevice(FanEntity): @property def oscillating(self): """Return the oscillation state.""" - return self._device.state and self._device.state.oscillation == "ON" + return self._device.state.oscillation == "ON" @property def is_on(self): """Return true if the entity is on.""" - if self._device.state: - return self._device.state.fan_mode in ["FAN", "AUTO"] - return False - - @property - def speed(self) -> str: - """Return the current speed.""" - if self._device.state: - if self._device.state.speed == FanSpeed.FAN_SPEED_AUTO.value: - return self._device.state.speed - return int(self._device.state.speed) - return None - - @property - def current_direction(self): - """Return direction of the fan [forward, reverse].""" - return None - - @property - def night_mode(self): - """Return Night mode.""" - return self._device.state.night_mode == "ON" + return self._device.state.fan_mode in ["FAN", "AUTO"] def set_night_mode(self, night_mode: bool) -> None: """Turn fan in night mode.""" @@ -300,64 +291,15 @@ class DysonPureCoolLinkDevice(FanEntity): else: self._device.set_configuration(fan_mode=FanMode.FAN) - @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - supported_speeds = [ - FanSpeed.FAN_SPEED_AUTO.value, - int(FanSpeed.FAN_SPEED_1.value), - int(FanSpeed.FAN_SPEED_2.value), - int(FanSpeed.FAN_SPEED_3.value), - int(FanSpeed.FAN_SPEED_4.value), - int(FanSpeed.FAN_SPEED_5.value), - int(FanSpeed.FAN_SPEED_6.value), - int(FanSpeed.FAN_SPEED_7.value), - int(FanSpeed.FAN_SPEED_8.value), - int(FanSpeed.FAN_SPEED_9.value), - int(FanSpeed.FAN_SPEED_10.value), - ] - return supported_speeds - - @property - def supported_features(self) -> int: - """Flag supported features.""" - return SUPPORT_OSCILLATE | SUPPORT_SET_SPEED - - @property - def device_state_attributes(self) -> dict: - """Return optional state attributes.""" - return {ATTR_NIGHT_MODE: self.night_mode, ATTR_AUTO_MODE: self.auto_mode} - - -class DysonPureCoolDevice(FanEntity): +class DysonPureCoolEntity(DysonFanEntity): """Representation of a Dyson Purecool (TP04/DP04) fan.""" def __init__(self, device): """Initialize the fan.""" - self._device = device + super().__init__(device, DysonPureCoolV2State) - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self._device.add_message_listener(self.on_message) - - def on_message(self, message): - """Call when new messages received from the fan.""" - if isinstance(message, DysonPureCoolV2State): - _LOGGER.debug("Message received for fan device %s: %s", self.name, message) - self.schedule_update_ha_state() - - @property - def should_poll(self): - """No polling needed.""" - return False - - @property - def name(self): - """Return the display name of this fan.""" - return self._device.name - - def turn_on(self, speed: str = None, **kwargs) -> None: + def turn_on(self, speed: Optional[str] = None, **kwargs) -> None: """Turn on the fan.""" _LOGGER.debug("Turn on fan %s", self.name) @@ -366,26 +308,14 @@ class DysonPureCoolDevice(FanEntity): else: self._device.turn_on() - def set_speed(self, speed: str) -> None: - """Set the speed of the fan.""" - if speed == SPEED_LOW: - self._device.set_fan_speed(FanSpeed.FAN_SPEED_4) - elif speed == SPEED_MEDIUM: - self._device.set_fan_speed(FanSpeed.FAN_SPEED_7) - elif speed == SPEED_HIGH: - self._device.set_fan_speed(FanSpeed.FAN_SPEED_10) - def turn_off(self, **kwargs): """Turn off the fan.""" _LOGGER.debug("Turn off fan %s", self.name) self._device.turn_off() - def set_dyson_speed(self, speed: str = None) -> None: + def set_dyson_speed(self, speed: FanSpeed) -> None: """Set the exact speed of the purecool fan.""" - _LOGGER.debug("Set exact speed for fan %s", self.name) - - fan_speed = FanSpeed(f"{int(speed):04d}") - self._device.set_fan_speed(fan_speed) + self._device.set_fan_speed(speed) def oscillate(self, oscillating: bool) -> None: """Turn on/off oscillating.""" @@ -453,40 +383,7 @@ class DysonPureCoolDevice(FanEntity): @property def is_on(self): """Return true if the entity is on.""" - if self._device.state: - return self._device.state.fan_power == "ON" - - @property - def speed(self): - """Return the current speed.""" - speed_map = { - FanSpeed.FAN_SPEED_1.value: SPEED_LOW, - FanSpeed.FAN_SPEED_2.value: SPEED_LOW, - FanSpeed.FAN_SPEED_3.value: SPEED_LOW, - FanSpeed.FAN_SPEED_4.value: SPEED_LOW, - FanSpeed.FAN_SPEED_AUTO.value: SPEED_MEDIUM, - FanSpeed.FAN_SPEED_5.value: SPEED_MEDIUM, - FanSpeed.FAN_SPEED_6.value: SPEED_MEDIUM, - FanSpeed.FAN_SPEED_7.value: SPEED_MEDIUM, - FanSpeed.FAN_SPEED_8.value: SPEED_HIGH, - FanSpeed.FAN_SPEED_9.value: SPEED_HIGH, - FanSpeed.FAN_SPEED_10.value: SPEED_HIGH, - } - - return speed_map[self._device.state.speed] - - @property - def dyson_speed(self): - """Return the current speed.""" - if self._device.state: - if self._device.state.speed == FanSpeed.FAN_SPEED_AUTO.value: - return self._device.state.speed - return int(self._device.state.speed) - - @property - def night_mode(self): - """Return Night mode.""" - return self._device.state.night_mode == "ON" + return self._device.state.fan_power == "ON" @property def auto_mode(self): @@ -525,49 +422,15 @@ class DysonPureCoolDevice(FanEntity): return self._device.state.carbon_filter_state return int(self._device.state.carbon_filter_state) - @property - def speed_list(self) -> list: - """Get the list of available speeds.""" - return [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] - - @property - def dyson_speed_list(self) -> list: - """Get the list of available dyson speeds.""" - return [ - int(FanSpeed.FAN_SPEED_1.value), - int(FanSpeed.FAN_SPEED_2.value), - int(FanSpeed.FAN_SPEED_3.value), - int(FanSpeed.FAN_SPEED_4.value), - int(FanSpeed.FAN_SPEED_5.value), - int(FanSpeed.FAN_SPEED_6.value), - int(FanSpeed.FAN_SPEED_7.value), - int(FanSpeed.FAN_SPEED_8.value), - int(FanSpeed.FAN_SPEED_9.value), - int(FanSpeed.FAN_SPEED_10.value), - ] - - @property - def device_serial(self): - """Return fan's serial number.""" - return self._device.serial - - @property - def supported_features(self) -> int: - """Flag supported features.""" - return SUPPORT_OSCILLATE | SUPPORT_SET_SPEED - @property def device_state_attributes(self) -> dict: """Return optional state attributes.""" return { - ATTR_NIGHT_MODE: self.night_mode, - ATTR_AUTO_MODE: self.auto_mode, + **super().device_state_attributes, ATTR_ANGLE_LOW: self.angle_low, ATTR_ANGLE_HIGH: self.angle_high, ATTR_FLOW_DIRECTION_FRONT: self.flow_direction_front, ATTR_TIMER: self.timer, ATTR_HEPA_FILTER: self.hepa_filter, ATTR_CARBON_FILTER: self.carbon_filter, - ATTR_DYSON_SPEED: self.dyson_speed, - ATTR_DYSON_SPEED_LIST: self.dyson_speed_list, } diff --git a/homeassistant/components/dyson/sensor.py b/homeassistant/components/dyson/sensor.py index 1629d0fa06b..f1198188b5c 100644 --- a/homeassistant/components/dyson/sensor.py +++ b/homeassistant/components/dyson/sensor.py @@ -4,28 +4,56 @@ import logging from libpurecool.dyson_pure_cool import DysonPureCool from libpurecool.dyson_pure_cool_link import DysonPureCoolLink -from homeassistant.const import PERCENTAGE, STATE_OFF, TEMP_CELSIUS, TIME_HOURS +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, + STATE_OFF, + TEMP_CELSIUS, + TIME_HOURS, +) from homeassistant.helpers.entity import Entity -from . import DYSON_DEVICES +from . import DYSON_DEVICES, DysonEntity -SENSOR_UNITS = { - "air_quality": None, - "dust": None, - "filter_life": TIME_HOURS, - "carbon_filter_state": PERCENTAGE, - "hepa_filter_state": PERCENTAGE, - "humidity": PERCENTAGE, +SENSOR_ATTRIBUTES = { + "air_quality": {ATTR_ICON: "mdi:fan"}, + "dust": {ATTR_ICON: "mdi:cloud"}, + "humidity": { + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + }, + "temperature": {ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE}, + "filter_life": { + ATTR_ICON: "mdi:filter-outline", + ATTR_UNIT_OF_MEASUREMENT: TIME_HOURS, + }, + "carbon_filter_state": { + ATTR_ICON: "mdi:filter-outline", + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + }, + "combi_filter_state": { + ATTR_ICON: "mdi:filter-outline", + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + }, + "hepa_filter_state": { + ATTR_ICON: "mdi:filter-outline", + ATTR_UNIT_OF_MEASUREMENT: PERCENTAGE, + }, } -SENSOR_ICONS = { - "air_quality": "mdi:fan", - "dust": "mdi:cloud", - "filter_life": "mdi:filter-outline", - "carbon_filter_state": "mdi:filter-outline", - "hepa_filter_state": "mdi:filter-outline", - "humidity": "mdi:water-percent", - "temperature": "mdi:thermometer", +SENSOR_NAMES = { + "air_quality": "AQI", + "dust": "Dust", + "humidity": "Humidity", + "temperature": "Temperature", + "filter_life": "Filter Life", + "carbon_filter_state": "Carbon Filter Remaining Life", + "combi_filter_state": "Combi Filter Remaining Life", + "hepa_filter_state": "HEPA Filter Remaining Life", } DYSON_SENSOR_DEVICES = "dyson_sensor_devices" @@ -57,7 +85,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # It's reported with the HEPA state, while the Carbon state is set to INValid. if device.state and device.state.carbon_filter_state == "INV": if f"{device.serial}-hepa_filter_state" not in device_ids: - new_entities.append(DysonHepaFilterLifeSensor(device, "Combi")) + new_entities.append(DysonHepaFilterLifeSensor(device, "combi")) else: if f"{device.serial}-hepa_filter_state" not in device_ids: new_entities.append(DysonHepaFilterLifeSensor(device)) @@ -77,53 +105,48 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(devices) -class DysonSensor(Entity): +class DysonSensor(DysonEntity, Entity): """Representation of a generic Dyson sensor.""" def __init__(self, device, sensor_type): """Create a new generic Dyson sensor.""" - self._device = device + super().__init__(device, None) self._old_value = None - self._name = None self._sensor_type = sensor_type - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self._device.add_message_listener(self.on_message) + self._attributes = SENSOR_ATTRIBUTES[sensor_type] def on_message(self, message): """Handle new messages which are received from the fan.""" # Prevent refreshing if not needed if self._old_value is None or self._old_value != self.state: - _LOGGER.debug("Message received for %s device: %s", self.name, message) self._old_value = self.state self.schedule_update_ha_state() - @property - def should_poll(self): - """No polling needed.""" - return False - @property def name(self): """Return the name of the Dyson sensor name.""" - return self._name - - @property - def unit_of_measurement(self): - """Return the unit the value is expressed in.""" - return SENSOR_UNITS[self._sensor_type] - - @property - def icon(self): - """Return the icon for this sensor.""" - return SENSOR_ICONS[self._sensor_type] + return f"{super().name} {SENSOR_NAMES[self._sensor_type]}" @property def unique_id(self): """Return the sensor's unique id.""" return f"{self._device.serial}-{self._sensor_type}" + @property + def unit_of_measurement(self): + """Return the unit the value is expressed in.""" + return self._attributes.get(ATTR_UNIT_OF_MEASUREMENT) + + @property + def icon(self): + """Return the icon for this sensor.""" + return self._attributes.get(ATTR_ICON) + + @property + def device_class(self): + """Return the device class of this sensor.""" + return self._attributes.get(ATTR_DEVICE_CLASS) + class DysonFilterLifeSensor(DysonSensor): """Representation of Dyson Filter Life sensor (in hours).""" @@ -131,14 +154,11 @@ class DysonFilterLifeSensor(DysonSensor): def __init__(self, device): """Create a new Dyson Filter Life sensor.""" super().__init__(device, "filter_life") - self._name = f"{self._device.name} Filter Life" @property def state(self): """Return filter life in hours.""" - if self._device.state: - return int(self._device.state.filter_life) - return None + return int(self._device.state.filter_life) class DysonCarbonFilterLifeSensor(DysonSensor): @@ -147,30 +167,24 @@ class DysonCarbonFilterLifeSensor(DysonSensor): def __init__(self, device): """Create a new Dyson Carbon Filter Life sensor.""" super().__init__(device, "carbon_filter_state") - self._name = f"{self._device.name} Carbon Filter Remaining Life" @property def state(self): """Return filter life remaining in percent.""" - if self._device.state: - return int(self._device.state.carbon_filter_state) - return None + return int(self._device.state.carbon_filter_state) class DysonHepaFilterLifeSensor(DysonSensor): """Representation of Dyson HEPA (or Combi) Filter Life sensor (in percent).""" - def __init__(self, device, filter_type="HEPA"): + def __init__(self, device, filter_type="hepa"): """Create a new Dyson Filter Life sensor.""" - super().__init__(device, "hepa_filter_state") - self._name = f"{self._device.name} {filter_type} Filter Remaining Life" + super().__init__(device, f"{filter_type}_filter_state") @property def state(self): """Return filter life remaining in percent.""" - if self._device.state: - return int(self._device.state.hepa_filter_state) - return None + return int(self._device.state.hepa_filter_state) class DysonDustSensor(DysonSensor): @@ -179,14 +193,11 @@ class DysonDustSensor(DysonSensor): def __init__(self, device): """Create a new Dyson Dust sensor.""" super().__init__(device, "dust") - self._name = f"{self._device.name} Dust" @property def state(self): """Return Dust value.""" - if self._device.environmental_state: - return self._device.environmental_state.dust - return None + return self._device.environmental_state.dust class DysonHumiditySensor(DysonSensor): @@ -195,16 +206,13 @@ class DysonHumiditySensor(DysonSensor): def __init__(self, device): """Create a new Dyson Humidity sensor.""" super().__init__(device, "humidity") - self._name = f"{self._device.name} Humidity" @property def state(self): """Return Humidity value.""" - if self._device.environmental_state: - if self._device.environmental_state.humidity == 0: - return STATE_OFF - return self._device.environmental_state.humidity - return None + if self._device.environmental_state.humidity == 0: + return STATE_OFF + return self._device.environmental_state.humidity class DysonTemperatureSensor(DysonSensor): @@ -213,20 +221,17 @@ class DysonTemperatureSensor(DysonSensor): def __init__(self, device, unit): """Create a new Dyson Temperature sensor.""" super().__init__(device, "temperature") - self._name = f"{self._device.name} Temperature" self._unit = unit @property def state(self): """Return Temperature value.""" - if self._device.environmental_state: - temperature_kelvin = self._device.environmental_state.temperature - if temperature_kelvin == 0: - return STATE_OFF - if self._unit == TEMP_CELSIUS: - return float(f"{(temperature_kelvin - 273.15):.1f}") - return float(f"{(temperature_kelvin * 9 / 5 - 459.67):.1f}") - return None + temperature_kelvin = self._device.environmental_state.temperature + if temperature_kelvin == 0: + return STATE_OFF + if self._unit == TEMP_CELSIUS: + return float(f"{(temperature_kelvin - 273.15):.1f}") + return float(f"{(temperature_kelvin * 9 / 5 - 459.67):.1f}") @property def unit_of_measurement(self): @@ -240,11 +245,8 @@ class DysonAirQualitySensor(DysonSensor): def __init__(self, device): """Create a new Dyson Air Quality sensor.""" super().__init__(device, "air_quality") - self._name = f"{self._device.name} AQI" @property def state(self): """Return Air Quality value.""" - if self._device.environmental_state: - return int(self._device.environmental_state.volatil_organic_compounds) - return None + return int(self._device.environmental_state.volatil_organic_compounds) diff --git a/homeassistant/components/dyson/vacuum.py b/homeassistant/components/dyson/vacuum.py index a3db4f9c4d3..466b409c342 100644 --- a/homeassistant/components/dyson/vacuum.py +++ b/homeassistant/components/dyson/vacuum.py @@ -17,7 +17,7 @@ from homeassistant.components.vacuum import ( ) from homeassistant.helpers.icon import icon_for_battery_level -from . import DYSON_DEVICES +from . import DYSON_DEVICES, DysonEntity _LOGGER = logging.getLogger(__name__) @@ -54,35 +54,12 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return True -class Dyson360EyeDevice(VacuumEntity): +class Dyson360EyeDevice(DysonEntity, VacuumEntity): """Dyson 360 Eye robot vacuum device.""" def __init__(self, device): """Dyson 360 Eye robot vacuum device.""" - _LOGGER.debug("Creating device %s", device.name) - self._device = device - - async def async_added_to_hass(self): - """Call when entity is added to hass.""" - self._device.add_message_listener(self.on_message) - - def on_message(self, message): - """Handle a new messages that was received from the vacuum.""" - _LOGGER.debug("Message received for %s device: %s", self.name, message) - self.schedule_update_ha_state() - - @property - def should_poll(self) -> bool: - """Return True if entity has to be polled for state. - - False if entity pushes its state to HA. - """ - return False - - @property - def name(self): - """Return the name of the device.""" - return self._device.name + super().__init__(device, None) @property def status(self): diff --git a/homeassistant/components/eafm/translations/de.json b/homeassistant/components/eafm/translations/de.json new file mode 100644 index 00000000000..da1d200c2a2 --- /dev/null +++ b/homeassistant/components/eafm/translations/de.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eafm/translations/tr.json b/homeassistant/components/eafm/translations/tr.json new file mode 100644 index 00000000000..4ed0f406e57 --- /dev/null +++ b/homeassistant/components/eafm/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "no_stations": "Ak\u0131\u015f izleme istasyonu bulunamad\u0131." + }, + "step": { + "user": { + "data": { + "station": "\u0130stasyon" + }, + "title": "Ak\u0131\u015f izleme istasyonunu takip edin" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/eafm/translations/uk.json b/homeassistant/components/eafm/translations/uk.json new file mode 100644 index 00000000000..4f84eb92722 --- /dev/null +++ b/homeassistant/components/eafm/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "no_stations": "\u0421\u0442\u0430\u043d\u0446\u0456\u0457 \u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433\u0443 \u043f\u043e\u0432\u0435\u043d\u0435\u0439 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456." + }, + "step": { + "user": { + "data": { + "station": "\u0421\u0442\u0430\u043d\u0446\u0456\u044f" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0442\u0430\u043d\u0446\u0456\u044e \u0434\u043b\u044f \u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433\u0443", + "title": "\u0421\u0442\u0430\u043d\u0446\u0456\u0457 \u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433\u0443 \u043f\u043e\u0432\u0435\u043d\u0435\u0439" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/climate.py b/homeassistant/components/ecobee/climate.py index 6bb7dc1a870..c61428cbc78 100644 --- a/homeassistant/components/ecobee/climate.py +++ b/homeassistant/components/ecobee/climate.py @@ -617,6 +617,7 @@ class Thermostat(ClimateEntity): cool_temp_setpoint, heat_temp_setpoint, self.hold_preference(), + self.hold_hours(), ) _LOGGER.debug( "Setting ecobee hold_temp to: heat=%s, is=%s, cool=%s, is=%s", @@ -717,15 +718,32 @@ class Thermostat(ClimateEntity): def hold_preference(self): """Return user preference setting for hold time.""" - # Values returned from thermostat are 'useEndTime4hour', - # 'useEndTime2hour', 'nextTransition', 'indefinite', 'askMe' - default = self.thermostat["settings"]["holdAction"] - if default == "nextTransition": - return default - # add further conditions if other hold durations should be - # supported; note that this should not include 'indefinite' - # as an indefinite away hold is interpreted as away_mode - return "nextTransition" + # Values returned from thermostat are: + # "useEndTime2hour", "useEndTime4hour" + # "nextPeriod", "askMe" + # "indefinite" + device_preference = self.thermostat["settings"]["holdAction"] + # Currently supported pyecobee holdTypes: + # dateTime, nextTransition, indefinite, holdHours + hold_pref_map = { + "useEndTime2hour": "holdHours", + "useEndTime4hour": "holdHours", + "indefinite": "indefinite", + } + return hold_pref_map.get(device_preference, "nextTransition") + + def hold_hours(self): + """Return user preference setting for hold duration in hours.""" + # Values returned from thermostat are: + # "useEndTime2hour", "useEndTime4hour" + # "nextPeriod", "askMe" + # "indefinite" + device_preference = self.thermostat["settings"]["holdAction"] + hold_hours_map = { + "useEndTime2hour": 2, + "useEndTime4hour": 4, + } + return hold_hours_map.get(device_preference, 0) def create_vacation(self, service_data): """Create a vacation with user-specified parameters.""" diff --git a/homeassistant/components/ecobee/translations/de.json b/homeassistant/components/ecobee/translations/de.json index bc65fddebdd..0c89a696b2c 100644 --- a/homeassistant/components/ecobee/translations/de.json +++ b/homeassistant/components/ecobee/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits eingerichtet. Es ist nur eine Konfiguration m\u00f6glich." + }, "error": { "pin_request_failed": "Fehler beim Anfordern der PIN von ecobee; Bitte \u00fcberpr\u00fcfe, ob der API-Schl\u00fcssel korrekt ist.", "token_request_failed": "Fehler beim Anfordern eines Token von ecobee; Bitte versuche es erneut." diff --git a/homeassistant/components/ecobee/translations/tr.json b/homeassistant/components/ecobee/translations/tr.json new file mode 100644 index 00000000000..23ece38682d --- /dev/null +++ b/homeassistant/components/ecobee/translations/tr.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ecobee/translations/uk.json b/homeassistant/components/ecobee/translations/uk.json new file mode 100644 index 00000000000..7cf7df53429 --- /dev/null +++ b/homeassistant/components/ecobee/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "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." + }, + "error": { + "pin_request_failed": "\u0421\u0442\u0430\u043b\u0430\u0441\u044f \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0456\u0434 \u0447\u0430\u0441 \u0437\u0430\u043f\u0438\u0442\u0443 PIN-\u043a\u043e\u0434\u0443 \u0443 ecobee; \u0431\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0456\u0441\u0442\u044c \u043a\u043b\u044e\u0447\u0430 API.", + "token_request_failed": "\u0421\u0442\u0430\u043b\u0430\u0441\u044f \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0456\u0434 \u0447\u0430\u0441 \u0437\u0430\u043f\u0438\u0442\u0443 \u0442\u043e\u043a\u0435\u043d\u0456\u0432 \u0443 ecobee; \u0431\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437." + }, + "step": { + "authorize": { + "description": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0440\u043e\u0439\u0434\u0456\u0442\u044c \u0437\u0430 \u0430\u0434\u0440\u0435\u0441\u043e\u044e https://www.ecobee.com/consumerportal/index.html \u0456 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e PIN-\u043a\u043e\u0434\u0443: \n\n{pin}\n\n\u041f\u043e\u0442\u0456\u043c \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u041d\u0430\u0434\u0456\u0441\u043b\u0430\u0442\u0438.", + "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u044f \u0434\u043e\u0434\u0430\u0442\u043a\u0430 \u043d\u0430 ecobee.com" + }, + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043a\u043b\u044e\u0447 API, \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u0438\u0439 \u0432\u0456\u0434 ecobee.com.", + "title": "ecobee" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/__init__.py b/homeassistant/components/econet/__init__.py index 48b7dad4c7c..dce4550eb1b 100644 --- a/homeassistant/components/econet/__init__.py +++ b/homeassistant/components/econet/__init__.py @@ -1 +1,158 @@ -"""The econet component.""" +"""Support for EcoNet products.""" +import asyncio +from datetime import timedelta +import logging + +from aiohttp.client_exceptions import ClientError +from pyeconet import EcoNetApiInterface +from pyeconet.equipment import EquipmentType +from pyeconet.errors import ( + GenericHTTPError, + InvalidCredentialsError, + InvalidResponseFormat, + PyeconetError, +) + +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.core import callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_track_time_interval + +from .const import API_CLIENT, DOMAIN, EQUIPMENT + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["binary_sensor", "sensor", "water_heater"] +PUSH_UPDATE = "econet.push_update" + +INTERVAL = timedelta(minutes=60) + + +async def async_setup(hass, config): + """Set up the EcoNet component.""" + hass.data[DOMAIN] = {} + hass.data[DOMAIN][API_CLIENT] = {} + hass.data[DOMAIN][EQUIPMENT] = {} + return True + + +async def async_setup_entry(hass, config_entry): + """Set up EcoNet as config entry.""" + + email = config_entry.data[CONF_EMAIL] + password = config_entry.data[CONF_PASSWORD] + + try: + api = await EcoNetApiInterface.login(email, password=password) + except InvalidCredentialsError: + _LOGGER.error("Invalid credentials provided") + return False + except PyeconetError as err: + _LOGGER.error("Config entry failed: %s", err) + raise ConfigEntryNotReady from err + + try: + equipment = await api.get_equipment_by_type([EquipmentType.WATER_HEATER]) + except (ClientError, GenericHTTPError, InvalidResponseFormat) as err: + raise ConfigEntryNotReady from err + hass.data[DOMAIN][API_CLIENT][config_entry.entry_id] = api + hass.data[DOMAIN][EQUIPMENT][config_entry.entry_id] = equipment + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, component) + ) + + api.subscribe() + + def update_published(): + """Handle a push update.""" + dispatcher_send(hass, PUSH_UPDATE) + + for _eqip in equipment[EquipmentType.WATER_HEATER]: + _eqip.set_update_callback(update_published) + + async def resubscribe(now): + """Resubscribe to the MQTT updates.""" + await hass.async_add_executor_job(api.unsubscribe) + api.subscribe() + + async def fetch_update(now): + """Fetch the latest changes from the API.""" + await api.refresh_equipment() + + async_track_time_interval(hass, resubscribe, INTERVAL) + async_track_time_interval(hass, fetch_update, INTERVAL + timedelta(minutes=1)) + + return True + + +async def async_unload_entry(hass, entry): + """Unload a EcoNet config entry.""" + tasks = [ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + + await asyncio.gather(*tasks) + + hass.data[DOMAIN][API_CLIENT].pop(entry.entry_id) + hass.data[DOMAIN][EQUIPMENT].pop(entry.entry_id) + + return True + + +class EcoNetEntity(Entity): + """Define a base EcoNet entity.""" + + def __init__(self, econet): + """Initialize.""" + self._econet = econet + + async def async_added_to_hass(self): + """Subscribe to device events.""" + await super().async_added_to_hass() + self.async_on_remove( + self.hass.helpers.dispatcher.async_dispatcher_connect( + PUSH_UPDATE, self.on_update_received + ) + ) + + @callback + def on_update_received(self): + """Update was pushed from the ecoent API.""" + self.async_write_ha_state() + + @property + def available(self): + """Return if the the device is online or not.""" + return self._econet.connected + + @property + def device_info(self): + """Return device registry information for this entity.""" + return { + "identifiers": {(DOMAIN, self._econet.device_id)}, + "manufacturer": "Rheem", + "name": self._econet.device_name, + } + + @property + def name(self): + """Return the name of the entity.""" + return self._econet.device_name + + @property + def unique_id(self): + """Return the unique ID of the entity.""" + return f"{self._econet.device_id}_{self._econet.device_name}" + + @property + def should_poll(self) -> bool: + """Return True if entity has to be polled for state. + + False if entity pushes its state to HA. + """ + return False diff --git a/homeassistant/components/econet/binary_sensor.py b/homeassistant/components/econet/binary_sensor.py new file mode 100644 index 00000000000..ec8131c5105 --- /dev/null +++ b/homeassistant/components/econet/binary_sensor.py @@ -0,0 +1,82 @@ +"""Support for Rheem EcoNet water heaters.""" +import logging + +from pyeconet.equipment import EquipmentType + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_OPENING, + DEVICE_CLASS_POWER, + BinarySensorEntity, +) + +from . import EcoNetEntity +from .const import DOMAIN, EQUIPMENT + +_LOGGER = logging.getLogger(__name__) + +SENSOR_NAME_RUNNING = "running" +SENSOR_NAME_SHUTOFF_VALVE = "shutoff_valve" +SENSOR_NAME_VACATION = "vacation" + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up EcoNet binary sensor based on a config entry.""" + equipment = hass.data[DOMAIN][EQUIPMENT][entry.entry_id] + binary_sensors = [] + for water_heater in equipment[EquipmentType.WATER_HEATER]: + if water_heater.has_shutoff_valve: + binary_sensors.append( + EcoNetBinarySensor( + water_heater, + SENSOR_NAME_SHUTOFF_VALVE, + ) + ) + if water_heater.running is not None: + binary_sensors.append(EcoNetBinarySensor(water_heater, SENSOR_NAME_RUNNING)) + if water_heater.vacation is not None: + binary_sensors.append( + EcoNetBinarySensor(water_heater, SENSOR_NAME_VACATION) + ) + async_add_entities(binary_sensors) + + +class EcoNetBinarySensor(EcoNetEntity, BinarySensorEntity): + """Define a Econet binary sensor.""" + + def __init__(self, econet_device, device_name): + """Initialize.""" + super().__init__(econet_device) + self._econet = econet_device + self._device_name = device_name + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + if self._device_name == SENSOR_NAME_SHUTOFF_VALVE: + return self._econet.shutoff_valve_open + if self._device_name == SENSOR_NAME_RUNNING: + return self._econet.running + if self._device_name == SENSOR_NAME_VACATION: + return self._econet.vacation + return False + + @property + def device_class(self): + """Return the class of this sensor, from DEVICE_CLASSES.""" + if self._device_name == SENSOR_NAME_SHUTOFF_VALVE: + return DEVICE_CLASS_OPENING + if self._device_name == SENSOR_NAME_RUNNING: + return DEVICE_CLASS_POWER + return None + + @property + def name(self): + """Return the name of the entity.""" + return f"{self._econet.device_name}_{self._device_name}" + + @property + def unique_id(self): + """Return the unique ID of the entity.""" + return ( + f"{self._econet.device_id}_{self._econet.device_name}_{self._device_name}" + ) diff --git a/homeassistant/components/econet/config_flow.py b/homeassistant/components/econet/config_flow.py new file mode 100644 index 00000000000..78aff2eac8f --- /dev/null +++ b/homeassistant/components/econet/config_flow.py @@ -0,0 +1,61 @@ +"""Config flow to configure the EcoNet component.""" +from pyeconet import EcoNetApiInterface +from pyeconet.errors import InvalidCredentialsError, PyeconetError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD + +from .const import DOMAIN # pylint: disable=unused-import + + +class EcoNetFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle an EcoNet config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize the config flow.""" + self.data_schema = vol.Schema( + { + vol.Required(CONF_EMAIL): str, + vol.Required(CONF_PASSWORD): str, + } + ) + + async def async_step_user(self, user_input=None): + """Handle the start of the config flow.""" + if not user_input: + return self.async_show_form( + step_id="user", + data_schema=self.data_schema, + ) + + await self.async_set_unique_id(user_input[CONF_EMAIL]) + self._abort_if_unique_id_configured() + errors = {} + + try: + await EcoNetApiInterface.login( + user_input[CONF_EMAIL], user_input[CONF_PASSWORD] + ) + except InvalidCredentialsError: + errors["base"] = "invalid_auth" + except PyeconetError: + errors["base"] = "cannot_connect" + + if errors: + return self.async_show_form( + step_id="user", + data_schema=self.data_schema, + errors=errors, + ) + + return self.async_create_entry( + title=user_input[CONF_EMAIL], + data={ + CONF_EMAIL: user_input[CONF_EMAIL], + CONF_PASSWORD: user_input[CONF_PASSWORD], + }, + ) diff --git a/homeassistant/components/econet/const.py b/homeassistant/components/econet/const.py index 88b1b851aa6..46c70021048 100644 --- a/homeassistant/components/econet/const.py +++ b/homeassistant/components/econet/const.py @@ -1,5 +1,5 @@ """Constants for Econet integration.""" DOMAIN = "econet" -SERVICE_ADD_VACATION = "add_vacation" -SERVICE_DELETE_VACATION = "delete_vacation" +API_CLIENT = "api_client" +EQUIPMENT = "equipment" diff --git a/homeassistant/components/econet/manifest.json b/homeassistant/components/econet/manifest.json index 21476d2b7ff..7e4cf0106ba 100644 --- a/homeassistant/components/econet/manifest.json +++ b/homeassistant/components/econet/manifest.json @@ -1,7 +1,9 @@ + { "domain": "econet", - "name": "Rheem EcoNET Water Products", + "name": "Rheem EcoNet Products", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/econet", - "requirements": ["pyeconet==0.0.11"], - "codeowners": [] -} + "requirements": ["pyeconet==0.1.12"], + "codeowners": ["@vangorra", "@w1ll1am23"] +} \ No newline at end of file diff --git a/homeassistant/components/econet/sensor.py b/homeassistant/components/econet/sensor.py new file mode 100644 index 00000000000..6ae14d18aa1 --- /dev/null +++ b/homeassistant/components/econet/sensor.py @@ -0,0 +1,122 @@ +"""Support for Rheem EcoNet water heaters.""" +import logging + +from pyeconet.equipment import EquipmentType + +from homeassistant.const import ( + ENERGY_KILO_WATT_HOUR, + PERCENTAGE, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + VOLUME_GALLONS, +) + +from . import EcoNetEntity +from .const import DOMAIN, EQUIPMENT + +ENERGY_KILO_BRITISH_THERMAL_UNIT = "kBtu" +TANK_HEALTH = "tank_health" +AVAILIBLE_HOT_WATER = "availible_hot_water" +COMPRESSOR_HEALTH = "compressor_health" +OVERRIDE_STATUS = "oveerride_status" +WATER_USAGE_TODAY = "water_usage_today" +POWER_USAGE_TODAY = "power_usage_today" +ALERT_COUNT = "alert_count" +WIFI_SIGNAL = "wifi_signal" +RUNNING_STATE = "running_state" + + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up EcoNet sensor based on a config entry.""" + equipment = hass.data[DOMAIN][EQUIPMENT][entry.entry_id] + sensors = [] + for water_heater in equipment[EquipmentType.WATER_HEATER]: + if water_heater.tank_hot_water_availability is not None: + sensors.append(EcoNetSensor(water_heater, AVAILIBLE_HOT_WATER)) + if water_heater.tank_health is not None: + sensors.append(EcoNetSensor(water_heater, TANK_HEALTH)) + if water_heater.compressor_health is not None: + sensors.append(EcoNetSensor(water_heater, COMPRESSOR_HEALTH)) + if water_heater.override_status: + sensors.append(EcoNetSensor(water_heater, OVERRIDE_STATUS)) + if water_heater.running_state is not None: + sensors.append(EcoNetSensor(water_heater, RUNNING_STATE)) + # All units have this + sensors.append(EcoNetSensor(water_heater, ALERT_COUNT)) + # These aren't part of the device and start off as None in pyeconet so always add them + sensors.append(EcoNetSensor(water_heater, WATER_USAGE_TODAY)) + sensors.append(EcoNetSensor(water_heater, POWER_USAGE_TODAY)) + sensors.append(EcoNetSensor(water_heater, WIFI_SIGNAL)) + async_add_entities(sensors) + + +class EcoNetSensor(EcoNetEntity): + """Define a Econet sensor.""" + + def __init__(self, econet_device, device_name): + """Initialize.""" + super().__init__(econet_device) + self._econet = econet_device + self._device_name = device_name + + @property + def state(self): + """Return sensors state.""" + if self._device_name == AVAILIBLE_HOT_WATER: + return self._econet.tank_hot_water_availability + if self._device_name == TANK_HEALTH: + return self._econet.tank_health + if self._device_name == COMPRESSOR_HEALTH: + return self._econet.compressor_health + if self._device_name == OVERRIDE_STATUS: + return self._econet.oveerride_status + if self._device_name == WATER_USAGE_TODAY: + if self._econet.todays_water_usage: + return round(self._econet.todays_water_usage, 2) + return None + if self._device_name == POWER_USAGE_TODAY: + if self._econet.todays_energy_usage: + return round(self._econet.todays_energy_usage, 2) + return None + if self._device_name == WIFI_SIGNAL: + if self._econet.wifi_signal: + return self._econet.wifi_signal + return None + if self._device_name == ALERT_COUNT: + return self._econet.alert_count + if self._device_name == RUNNING_STATE: + return self._econet.running_state + return None + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + if self._device_name == AVAILIBLE_HOT_WATER: + return PERCENTAGE + if self._device_name == TANK_HEALTH: + return PERCENTAGE + if self._device_name == COMPRESSOR_HEALTH: + return PERCENTAGE + if self._device_name == WATER_USAGE_TODAY: + return VOLUME_GALLONS + if self._device_name == POWER_USAGE_TODAY: + if self._econet.energy_type == ENERGY_KILO_BRITISH_THERMAL_UNIT.upper(): + return ENERGY_KILO_BRITISH_THERMAL_UNIT + return ENERGY_KILO_WATT_HOUR + if self._device_name == WIFI_SIGNAL: + return SIGNAL_STRENGTH_DECIBELS_MILLIWATT + return None + + @property + def name(self): + """Return the name of the entity.""" + return f"{self._econet.device_name}_{self._device_name}" + + @property + def unique_id(self): + """Return the unique ID of the entity.""" + return ( + f"{self._econet.device_id}_{self._econet.device_name}_{self._device_name}" + ) diff --git a/homeassistant/components/econet/services.yaml b/homeassistant/components/econet/services.yaml deleted file mode 100644 index b531764c290..00000000000 --- a/homeassistant/components/econet/services.yaml +++ /dev/null @@ -1,19 +0,0 @@ -add_vacation: - description: Add a vacation to your water heater. - fields: - entity_id: - description: Name(s) of entities to change. - example: "water_heater.econet" - start_date: - description: The timestamp of when the vacation should start. (Optional, defaults to now) - example: 1513186320 - end_date: - description: The timestamp of when the vacation should end. - example: 1513445520 - -delete_vacation: - description: Delete your existing vacation from your water heater. - fields: - entity_id: - description: Name(s) of entities to change. - example: "water_heater.econet" diff --git a/homeassistant/components/econet/strings.json b/homeassistant/components/econet/strings.json new file mode 100644 index 00000000000..9d043e47ebc --- /dev/null +++ b/homeassistant/components/econet/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + }, + "abort": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "step": { + "user": { + "title": "Setup Rheem EcoNet Account", + "data": { + "email": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/translations/ca.json b/homeassistant/components/econet/translations/ca.json new file mode 100644 index 00000000000..c53914f8cb9 --- /dev/null +++ b/homeassistant/components/econet/translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + }, + "step": { + "user": { + "data": { + "email": "Correu electr\u00f2nic", + "password": "Contrasenya" + }, + "title": "Configuraci\u00f3 del compte Rheem EcoNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/translations/en.json b/homeassistant/components/econet/translations/en.json new file mode 100644 index 00000000000..ad499b0e37c --- /dev/null +++ b/homeassistant/components/econet/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "Password" + }, + "title": "Setup Rheem EcoNet Account" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/translations/es.json b/homeassistant/components/econet/translations/es.json new file mode 100644 index 00000000000..ac69f8f7be1 --- /dev/null +++ b/homeassistant/components/econet/translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida" + }, + "step": { + "user": { + "data": { + "email": "Correo electr\u00f3nico", + "password": "Contrase\u00f1a" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/translations/et.json b/homeassistant/components/econet/translations/et.json new file mode 100644 index 00000000000..349a4d21111 --- /dev/null +++ b/homeassistant/components/econet/translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Vigane autentimine" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Vigane autentimine" + }, + "step": { + "user": { + "data": { + "email": "E-posti aadress", + "password": "Salas\u00f5na" + }, + "title": "Seadista Rheem EcoNeti konto" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/translations/it.json b/homeassistant/components/econet/translations/it.json new file mode 100644 index 00000000000..3074c72b083 --- /dev/null +++ b/homeassistant/components/econet/translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida" + }, + "step": { + "user": { + "data": { + "email": "E-mail", + "password": "Password" + }, + "title": "Imposta account Rheem EcoNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/translations/no.json b/homeassistant/components/econet/translations/no.json new file mode 100644 index 00000000000..f54cedffda8 --- /dev/null +++ b/homeassistant/components/econet/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning" + }, + "step": { + "user": { + "data": { + "email": "E-post", + "password": "Passord" + }, + "title": "Konfigurer Rheem EcoNet-konto" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/translations/pl.json b/homeassistant/components/econet/translations/pl.json new file mode 100644 index 00000000000..e5d74de590d --- /dev/null +++ b/homeassistant/components/econet/translations/pl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie" + }, + "step": { + "user": { + "data": { + "email": "Adres e-mail", + "password": "Has\u0142o" + }, + "title": "Konfiguracja konta Rheem EcoNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/translations/ru.json b/homeassistant/components/econet/translations/ru.json new file mode 100644 index 00000000000..109ded8db99 --- /dev/null +++ b/homeassistant/components/econet/translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." + }, + "step": { + "user": { + "data": { + "email": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "title": "Rheem EcoNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/translations/tr.json b/homeassistant/components/econet/translations/tr.json new file mode 100644 index 00000000000..237a87d0268 --- /dev/null +++ b/homeassistant/components/econet/translations/tr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "email": "Email", + "password": "\u015eifre" + }, + "title": "Rheem EcoNet Hesab\u0131n\u0131 Kur" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/translations/zh-Hant.json b/homeassistant/components/econet/translations/zh-Hant.json new file mode 100644 index 00000000000..50824c19814 --- /dev/null +++ b/homeassistant/components/econet/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "step": { + "user": { + "data": { + "email": "\u96fb\u5b50\u90f5\u4ef6", + "password": "\u5bc6\u78bc" + }, + "title": "\u8a2d\u5b9a Rheem EcoNet \u5e33\u865f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/econet/water_heater.py b/homeassistant/components/econet/water_heater.py index 0c31e3e50e0..af3399b53af 100644 --- a/homeassistant/components/econet/water_heater.py +++ b/homeassistant/components/econet/water_heater.py @@ -1,12 +1,11 @@ """Support for Rheem EcoNet water heaters.""" -import datetime import logging -from pyeconet.api import PyEcoNet -import voluptuous as vol +from pyeconet.equipment import EquipmentType +from pyeconet.equipment.water_heater import WaterHeaterOperationMode from homeassistant.components.water_heater import ( - PLATFORM_SCHEMA, + ATTR_TEMPERATURE, STATE_ECO, STATE_ELECTRIC, STATE_GAS, @@ -14,222 +13,125 @@ from homeassistant.components.water_heater import ( STATE_HIGH_DEMAND, STATE_OFF, STATE_PERFORMANCE, + SUPPORT_AWAY_MODE, SUPPORT_OPERATION_MODE, SUPPORT_TARGET_TEMPERATURE, WaterHeaterEntity, ) -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_TEMPERATURE, - CONF_PASSWORD, - CONF_USERNAME, - TEMP_FAHRENHEIT, -) -import homeassistant.helpers.config_validation as cv +from homeassistant.const import TEMP_FAHRENHEIT +from homeassistant.core import callback -from .const import DOMAIN, SERVICE_ADD_VACATION, SERVICE_DELETE_VACATION +from . import EcoNetEntity +from .const import DOMAIN, EQUIPMENT _LOGGER = logging.getLogger(__name__) -ATTR_VACATION_START = "next_vacation_start_date" -ATTR_VACATION_END = "next_vacation_end_date" -ATTR_ON_VACATION = "on_vacation" -ATTR_TODAYS_ENERGY_USAGE = "todays_energy_usage" -ATTR_IN_USE = "in_use" - -ATTR_START_DATE = "start_date" -ATTR_END_DATE = "end_date" - -ATTR_LOWER_TEMP = "lower_temp" -ATTR_UPPER_TEMP = "upper_temp" -ATTR_IS_ENABLED = "is_enabled" +ECONET_STATE_TO_HA = { + WaterHeaterOperationMode.ENERGY_SAVING: STATE_ECO, + WaterHeaterOperationMode.HIGH_DEMAND: STATE_HIGH_DEMAND, + WaterHeaterOperationMode.OFF: STATE_OFF, + WaterHeaterOperationMode.HEAT_PUMP_ONLY: STATE_HEAT_PUMP, + WaterHeaterOperationMode.ELECTRIC_MODE: STATE_ELECTRIC, + WaterHeaterOperationMode.GAS: STATE_GAS, + WaterHeaterOperationMode.PERFORMANCE: STATE_PERFORMANCE, +} +HA_STATE_TO_ECONET = {value: key for key, value in ECONET_STATE_TO_HA.items()} SUPPORT_FLAGS_HEATER = SUPPORT_TARGET_TEMPERATURE | SUPPORT_OPERATION_MODE -ADD_VACATION_SCHEMA = vol.Schema( - { - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Optional(ATTR_START_DATE): cv.positive_int, - vol.Required(ATTR_END_DATE): cv.positive_int, - } -) -DELETE_VACATION_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) - -ECONET_DATA = "econet" - -ECONET_STATE_TO_HA = { - "Energy Saver": STATE_ECO, - "gas": STATE_GAS, - "High Demand": STATE_HIGH_DEMAND, - "Off": STATE_OFF, - "Performance": STATE_PERFORMANCE, - "Heat Pump Only": STATE_HEAT_PUMP, - "Electric-Only": STATE_ELECTRIC, - "Electric": STATE_ELECTRIC, - "Heat Pump": STATE_HEAT_PUMP, -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - {vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string} -) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the EcoNet water heaters.""" - - hass.data[ECONET_DATA] = {} - hass.data[ECONET_DATA]["water_heaters"] = [] - - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - - econet = PyEcoNet(username, password) - water_heaters = econet.get_water_heaters() - hass_water_heaters = [ - EcoNetWaterHeater(water_heater) for water_heater in water_heaters - ] - add_entities(hass_water_heaters) - hass.data[ECONET_DATA]["water_heaters"].extend(hass_water_heaters) - - def service_handle(service): - """Handle the service calls.""" - entity_ids = service.data.get("entity_id") - all_heaters = hass.data[ECONET_DATA]["water_heaters"] - _heaters = [ - x for x in all_heaters if not entity_ids or x.entity_id in entity_ids - ] - - for _water_heater in _heaters: - if service.service == SERVICE_ADD_VACATION: - start = service.data.get(ATTR_START_DATE) - end = service.data.get(ATTR_END_DATE) - _water_heater.add_vacation(start, end) - if service.service == SERVICE_DELETE_VACATION: - for vacation in _water_heater.water_heater.vacations: - vacation.delete() - - _water_heater.schedule_update_ha_state(True) - - hass.services.register( - DOMAIN, SERVICE_ADD_VACATION, service_handle, schema=ADD_VACATION_SCHEMA - ) - - hass.services.register( - DOMAIN, SERVICE_DELETE_VACATION, service_handle, schema=DELETE_VACATION_SCHEMA +async def async_setup_entry(hass, entry, async_add_entities): + """Set up EcoNet water heater based on a config entry.""" + equipment = hass.data[DOMAIN][EQUIPMENT][entry.entry_id] + async_add_entities( + [ + EcoNetWaterHeater(water_heater) + for water_heater in equipment[EquipmentType.WATER_HEATER] + ], ) -class EcoNetWaterHeater(WaterHeaterEntity): - """Representation of an EcoNet water heater.""" +class EcoNetWaterHeater(EcoNetEntity, WaterHeaterEntity): + """Define a Econet water heater.""" def __init__(self, water_heater): - """Initialize the water heater.""" + """Initialize.""" + super().__init__(water_heater) + self._running = water_heater.running + self._poll = True self.water_heater = water_heater - self.supported_modes = self.water_heater.supported_modes self.econet_state_to_ha = {} self.ha_state_to_econet = {} - for mode in ECONET_STATE_TO_HA: - if mode in self.supported_modes: - self.econet_state_to_ha[mode] = ECONET_STATE_TO_HA.get(mode) - for key, value in self.econet_state_to_ha.items(): - self.ha_state_to_econet[value] = key - for mode in self.supported_modes: - if mode not in ECONET_STATE_TO_HA: - error = f"Invalid operation mode mapping. {mode} doesn't map. Please report this." - _LOGGER.error(error) + + @callback + def on_update_received(self): + """Update was pushed from the ecoent API.""" + if self._running != self.water_heater.running: + # Water heater running state has changed so check usage on next update + self._poll = True + self._running = self.water_heater.running + self.async_write_ha_state() @property - def name(self): - """Return the device name.""" - return self.water_heater.name - - @property - def available(self): - """Return if the the device is online or not.""" - return self.water_heater.is_connected + def is_away_mode_on(self): + """Return true if away mode is on.""" + return self._econet.away @property def temperature_unit(self): """Return the unit of measurement.""" return TEMP_FAHRENHEIT - @property - def device_state_attributes(self): - """Return the optional device state attributes.""" - data = {} - vacations = self.water_heater.get_vacations() - if vacations: - data[ATTR_VACATION_START] = vacations[0].start_date - data[ATTR_VACATION_END] = vacations[0].end_date - data[ATTR_ON_VACATION] = self.water_heater.is_on_vacation - todays_usage = self.water_heater.total_usage_for_today - if todays_usage: - data[ATTR_TODAYS_ENERGY_USAGE] = todays_usage - data[ATTR_IN_USE] = self.water_heater.in_use - - if self.water_heater.lower_temp is not None: - data[ATTR_LOWER_TEMP] = round(self.water_heater.lower_temp, 2) - if self.water_heater.upper_temp is not None: - data[ATTR_UPPER_TEMP] = round(self.water_heater.upper_temp, 2) - if self.water_heater.is_enabled is not None: - data[ATTR_IS_ENABLED] = self.water_heater.is_enabled - - return data - @property def current_operation(self): - """ - Return current operation as one of the following. + """Return current operation.""" + econet_mode = self.water_heater.mode + _current_op = STATE_OFF + if econet_mode is not None: + _current_op = ECONET_STATE_TO_HA[econet_mode] - ["eco", "heat_pump", "high_demand", "electric_only"] - """ - current_op = self.econet_state_to_ha.get(self.water_heater.mode) - return current_op + return _current_op @property def operation_list(self): """List of available operation modes.""" + econet_modes = self.water_heater.modes op_list = [] - for mode in self.supported_modes: - ha_mode = self.econet_state_to_ha.get(mode) - if ha_mode is not None: + for mode in econet_modes: + if ( + mode is not WaterHeaterOperationMode.UNKNOWN + and mode is not WaterHeaterOperationMode.VACATION + ): + ha_mode = ECONET_STATE_TO_HA[mode] op_list.append(ha_mode) return op_list @property def supported_features(self): """Return the list of supported features.""" - return SUPPORT_FLAGS_HEATER + if self.water_heater.modes: + if self.water_heater.supports_away: + return SUPPORT_FLAGS_HEATER | SUPPORT_AWAY_MODE + return SUPPORT_FLAGS_HEATER + if self.water_heater.supports_away: + return SUPPORT_TARGET_TEMPERATURE | SUPPORT_AWAY_MODE + return SUPPORT_TARGET_TEMPERATURE def set_temperature(self, **kwargs): """Set new target temperature.""" target_temp = kwargs.get(ATTR_TEMPERATURE) if target_temp is not None: - self.water_heater.set_target_set_point(target_temp) + self.water_heater.set_set_point(target_temp) else: _LOGGER.error("A target temperature must be provided") def set_operation_mode(self, operation_mode): """Set operation mode.""" - op_mode_to_set = self.ha_state_to_econet.get(operation_mode) + op_mode_to_set = HA_STATE_TO_ECONET.get(operation_mode) if op_mode_to_set is not None: self.water_heater.set_mode(op_mode_to_set) else: - _LOGGER.error("An operation mode must be provided") - - def add_vacation(self, start, end): - """Add a vacation to this water heater.""" - if not start: - start = datetime.datetime.now() - else: - start = datetime.datetime.fromtimestamp(start) - end = datetime.datetime.fromtimestamp(end) - self.water_heater.set_vacation_mode(start, end) - - def update(self): - """Get the latest date.""" - self.water_heater.update_state() + _LOGGER.error("Invalid operation mode: %s", operation_mode) @property def target_temperature(self): @@ -239,9 +141,31 @@ class EcoNetWaterHeater(WaterHeaterEntity): @property def min_temp(self): """Return the minimum temperature.""" - return self.water_heater.min_set_point + return self.water_heater.set_point_limits[0] @property def max_temp(self): """Return the maximum temperature.""" - return self.water_heater.max_set_point + return self.water_heater.set_point_limits[1] + + @property + def should_poll(self) -> bool: + """Return True if entity has to be polled for state. + + False if entity pushes its state to HA. + """ + return self._poll + + async def async_update(self): + """Get the latest energy usage.""" + await self.water_heater.get_energy_usage() + await self.water_heater.get_water_usage() + self._poll = False + + def turn_away_mode_on(self): + """Turn away mode on.""" + self.water_heater.set_away_mode(True) + + def turn_away_mode_off(self): + """Turn away mode off.""" + self.water_heater.set_away_mode(False) diff --git a/homeassistant/components/elgato/translations/de.json b/homeassistant/components/elgato/translations/de.json index 74974604453..1df8f91ecd6 100644 --- a/homeassistant/components/elgato/translations/de.json +++ b/homeassistant/components/elgato/translations/de.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "Dieses Elgato Key Light-Ger\u00e4t ist bereits konfiguriert.", - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "flow_title": "Elgato Key Light: {serial_number}", "step": { diff --git a/homeassistant/components/elgato/translations/tr.json b/homeassistant/components/elgato/translations/tr.json new file mode 100644 index 00000000000..b2d1753fd68 --- /dev/null +++ b/homeassistant/components/elgato/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elgato/translations/uk.json b/homeassistant/components/elgato/translations/uk.json new file mode 100644 index 00000000000..978ff1a3100 --- /dev/null +++ b/homeassistant/components/elgato/translations/uk.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "flow_title": "Elgato Key Light: {serial_number}", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Elgato Key Light \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 Home Assistant." + }, + "zeroconf_confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 Elgato Key Light \u0437 \u0441\u0435\u0440\u0456\u0439\u043d\u0438\u043c \u043d\u043e\u043c\u0435\u0440\u043e\u043c `{serial_number}`?", + "title": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 Elgato Key Light" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/manifest.json b/homeassistant/components/elkm1/manifest.json index 769e5c37dd7..2077890d3d2 100644 --- a/homeassistant/components/elkm1/manifest.json +++ b/homeassistant/components/elkm1/manifest.json @@ -2,7 +2,7 @@ "domain": "elkm1", "name": "Elk-M1 Control", "documentation": "https://www.home-assistant.io/integrations/elkm1", - "requirements": ["elkm1-lib==0.8.8"], + "requirements": ["elkm1-lib==0.8.10"], "codeowners": ["@gwww", "@bdraco"], "config_flow": true } diff --git a/homeassistant/components/elkm1/translations/de.json b/homeassistant/components/elkm1/translations/de.json index 8c562a75026..8157a061d82 100644 --- a/homeassistant/components/elkm1/translations/de.json +++ b/homeassistant/components/elkm1/translations/de.json @@ -5,7 +5,7 @@ "already_configured": "Ein ElkM1 mit diesem Pr\u00e4fix ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/elkm1/translations/tr.json b/homeassistant/components/elkm1/translations/tr.json new file mode 100644 index 00000000000..9259220985b --- /dev/null +++ b/homeassistant/components/elkm1/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "address_already_configured": "Bu adrese sahip bir ElkM1 zaten yap\u0131land\u0131r\u0131lm\u0131\u015ft\u0131r", + "already_configured": "Bu \u00f6nek ile bir ElkM1 zaten yap\u0131land\u0131r\u0131lm\u0131\u015ft\u0131r" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/translations/uk.json b/homeassistant/components/elkm1/translations/uk.json new file mode 100644 index 00000000000..a8e711a4590 --- /dev/null +++ b/homeassistant/components/elkm1/translations/uk.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "address_already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u0437 \u0446\u0456\u0454\u044e \u0430\u0434\u0440\u0435\u0441\u043e\u044e \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435.", + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u0437 \u0446\u0438\u043c \u043f\u0440\u0435\u0444\u0456\u043a\u0441\u043e\u043c \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430, \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e \u043f\u043e\u0441\u043b\u0456\u0434\u043e\u0432\u043d\u0438\u0439 \u043f\u043e\u0440\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "prefix": "\u0423\u043d\u0456\u043a\u0430\u043b\u044c\u043d\u0438\u0439 \u043f\u0440\u0435\u0444\u0456\u043a\u0441 (\u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c, \u044f\u043a\u0449\u043e \u0443 \u0412\u0430\u0441 \u0442\u0456\u043b\u044c\u043a\u0438 \u043e\u0434\u0438\u043d ElkM1)", + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", + "temperature_unit": "\u041e\u0434\u0438\u043d\u0438\u0446\u044f \u0432\u0438\u043c\u0456\u0440\u0443 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0438", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0420\u044f\u0434\u043e\u043a \u0430\u0434\u0440\u0435\u0441\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0430 \u0431\u0443\u0442\u0438 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 'addres[:port]' \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0456\u0432 'secure' \u0456 'non-secure' (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: '192.168.1.1'). \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 'port' \u0432\u043a\u0430\u0437\u0443\u0432\u0430\u0442\u0438 \u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e, \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0432\u0456\u043d \u0434\u043e\u0440\u0456\u0432\u043d\u044e\u0454 2101 \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 'non-secure' \u0456 2601 \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 'secure'. \u0414\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 'serial' \u0430\u0434\u0440\u0435\u0441\u0430 \u043f\u043e\u0432\u0438\u043d\u043d\u0430 \u0431\u0443\u0442\u0438 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 'tty[:baud]' (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: '/dev/ttyS1'). \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 'baud' \u0432\u043a\u0430\u0437\u0443\u0432\u0430\u0442\u0438 \u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e, \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0432\u0456\u043d \u0434\u043e\u0440\u0456\u0432\u043d\u044e\u0454 115200.", + "title": "Elk-M1 Control" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/translations/de.json b/homeassistant/components/emulated_roku/translations/de.json index a0bfd9f83aa..39c8da5197f 100644 --- a/homeassistant/components/emulated_roku/translations/de.json +++ b/homeassistant/components/emulated_roku/translations/de.json @@ -8,7 +8,7 @@ "data": { "advertise_ip": "IP Adresse annoncieren", "advertise_port": "Port annoncieren", - "host_ip": "Host-IP", + "host_ip": "Host-IP-Adresse", "listen_port": "Listen-Port", "name": "Name", "upnp_bind_multicast": "Multicast binden (True/False)" diff --git a/homeassistant/components/emulated_roku/translations/tr.json b/homeassistant/components/emulated_roku/translations/tr.json new file mode 100644 index 00000000000..5307276a71d --- /dev/null +++ b/homeassistant/components/emulated_roku/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/translations/uk.json b/homeassistant/components/emulated_roku/translations/uk.json new file mode 100644 index 00000000000..a299f3a5ebc --- /dev/null +++ b/homeassistant/components/emulated_roku/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "step": { + "user": { + "data": { + "advertise_ip": "\u041e\u0433\u043e\u043b\u043e\u0448\u0443\u0432\u0430\u0442\u0438 IP", + "advertise_port": "\u041e\u0433\u043e\u043b\u043e\u0448\u0443\u0432\u0430\u0442\u0438 \u043f\u043e\u0440\u0442", + "host_ip": "\u0425\u043e\u0441\u0442", + "listen_port": "\u041f\u043e\u0440\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430", + "upnp_bind_multicast": "\u041f\u0440\u0438\u0432'\u044f\u0437\u0430\u0442\u0438 multicast (True / False)" + }, + "title": "EmulatedRoku" + } + } + }, + "title": "Emulated Roku" +} \ No newline at end of file diff --git a/homeassistant/components/enocean/translations/tr.json b/homeassistant/components/enocean/translations/tr.json new file mode 100644 index 00000000000..b4e6be555ff --- /dev/null +++ b/homeassistant/components/enocean/translations/tr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "invalid_dongle_path": "Ge\u00e7ersiz dongle yolu", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "invalid_dongle_path": "Bu yol i\u00e7in ge\u00e7erli bir dongle bulunamad\u0131" + }, + "step": { + "detect": { + "data": { + "path": "USB dongle yolu" + } + }, + "manual": { + "data": { + "path": "USB dongle yolu" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/enocean/translations/uk.json b/homeassistant/components/enocean/translations/uk.json new file mode 100644 index 00000000000..5c3e2d6eb6e --- /dev/null +++ b/homeassistant/components/enocean/translations/uk.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "invalid_dongle_path": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439 \u0448\u043b\u044f\u0445 \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e.", + "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." + }, + "error": { + "invalid_dongle_path": "\u041d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 \u0437\u0430 \u0446\u0438\u043c \u0448\u043b\u044f\u0445\u043e\u043c." + }, + "step": { + "detect": { + "data": { + "path": "\u0428\u043b\u044f\u0445 \u0434\u043e USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + }, + "title": "ENOcean" + }, + "manual": { + "data": { + "path": "\u0428\u043b\u044f\u0445 \u0434\u043e USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + }, + "title": "ENOcean" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/epson/translations/de.json b/homeassistant/components/epson/translations/de.json index c03615a39ff..a91e3831cdb 100644 --- a/homeassistant/components/epson/translations/de.json +++ b/homeassistant/components/epson/translations/de.json @@ -1,12 +1,14 @@ { "config": { "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "user": { "data": { - "name": "Name" + "host": "Host", + "name": "Name", + "port": "Port" } } } diff --git a/homeassistant/components/epson/translations/tr.json b/homeassistant/components/epson/translations/tr.json index aafc2e2b303..9ffd77fc50f 100644 --- a/homeassistant/components/epson/translations/tr.json +++ b/homeassistant/components/epson/translations/tr.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/epson/translations/uk.json b/homeassistant/components/epson/translations/uk.json new file mode 100644 index 00000000000..65566a8f4aa --- /dev/null +++ b/homeassistant/components/epson/translations/uk.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index fcfb4cf7ff1..c0c3d02ec56 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -225,6 +225,14 @@ async def _setup_auto_reconnect_logic( # When removing/disconnecting manually return + device_registry = await hass.helpers.device_registry.async_get_registry() + devices = dr.async_entries_for_config_entry(device_registry, entry.entry_id) + for device in devices: + # There is only one device in ESPHome + if device.disabled: + # Don't attempt to connect if it's disabled + return + data: RuntimeEntryData = hass.data[DOMAIN][entry.entry_id] for disconnect_cb in data.disconnect_callbacks: disconnect_cb() diff --git a/homeassistant/components/esphome/fan.py b/homeassistant/components/esphome/fan.py index df663f8b10a..32f445d23a2 100644 --- a/homeassistant/components/esphome/fan.py +++ b/homeassistant/components/esphome/fan.py @@ -1,13 +1,16 @@ """Support for ESPHome fans.""" from typing import List, Optional -from aioesphomeapi import FanInfo, FanSpeed, FanState +from aioesphomeapi import FanDirection, FanInfo, FanSpeed, FanState from homeassistant.components.fan import ( + DIRECTION_FORWARD, + DIRECTION_REVERSE, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, SPEED_OFF, + SUPPORT_DIRECTION, SUPPORT_OSCILLATE, SUPPORT_SET_SPEED, FanEntity, @@ -47,6 +50,14 @@ def _fan_speeds(): } +@esphome_map_enum +def _fan_directions(): + return { + FanDirection.FORWARD: DIRECTION_FORWARD, + FanDirection.REVERSE: DIRECTION_REVERSE, + } + + class EsphomeFan(EsphomeEntity, FanEntity): """A fan implementation for ESPHome.""" @@ -88,6 +99,12 @@ class EsphomeFan(EsphomeEntity, FanEntity): key=self._static_info.key, oscillating=oscillating ) + async def async_set_direction(self, direction: str): + """Set direction of the fan.""" + await self._client.fan_command( + key=self._static_info.key, direction=_fan_directions.from_hass(direction) + ) + # https://github.com/PyCQA/pylint/issues/3150 for all @esphome_state_property # pylint: disable=invalid-overridden-method @@ -110,6 +127,13 @@ class EsphomeFan(EsphomeEntity, FanEntity): return None return self._state.oscillating + @esphome_state_property + def current_direction(self) -> None: + """Return the current fan direction.""" + if not self._static_info.supports_direction: + return None + return _fan_directions.from_esphome(self._state.direction) + @property def speed_list(self) -> Optional[List[str]]: """Get the list of available speeds.""" @@ -125,4 +149,6 @@ class EsphomeFan(EsphomeEntity, FanEntity): flags |= SUPPORT_OSCILLATE if self._static_info.supports_speed: flags |= SUPPORT_SET_SPEED + if self._static_info.supports_direction: + flags |= SUPPORT_DIRECTION return flags diff --git a/homeassistant/components/esphome/manifest.json b/homeassistant/components/esphome/manifest.json index 123c7931e41..c69f4f4d8c6 100644 --- a/homeassistant/components/esphome/manifest.json +++ b/homeassistant/components/esphome/manifest.json @@ -3,7 +3,7 @@ "name": "ESPHome", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/esphome", - "requirements": ["aioesphomeapi==2.6.3"], + "requirements": ["aioesphomeapi==2.6.4"], "zeroconf": ["_esphomelib._tcp.local."], "codeowners": ["@OttoWinter"], "after_dependencies": ["zeroconf", "tag"] diff --git a/homeassistant/components/esphome/translations/de.json b/homeassistant/components/esphome/translations/de.json index 826574cb7e0..fdaea452c45 100644 --- a/homeassistant/components/esphome/translations/de.json +++ b/homeassistant/components/esphome/translations/de.json @@ -2,10 +2,11 @@ "config": { "abort": { "already_configured": "ESP ist bereits konfiguriert", - "already_in_progress": "Die ESP-Konfiguration wird bereits ausgef\u00fchrt" + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt" }, "error": { "connection_error": "Keine Verbindung zum ESP m\u00f6glich. Achte darauf, dass deine YAML-Datei eine Zeile 'api:' enth\u00e4lt.", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "resolve_error": "Adresse des ESP kann nicht aufgel\u00f6st werden. Wenn dieser Fehler weiterhin besteht, lege eine statische IP-Adresse fest: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" }, "flow_title": "ESPHome: {name}", diff --git a/homeassistant/components/esphome/translations/pt.json b/homeassistant/components/esphome/translations/pt.json index 6ff4d786447..60eeaa3f4b2 100644 --- a/homeassistant/components/esphome/translations/pt.json +++ b/homeassistant/components/esphome/translations/pt.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "O ESP j\u00e1 est\u00e1 configurado", + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer" }, "error": { diff --git a/homeassistant/components/esphome/translations/tr.json b/homeassistant/components/esphome/translations/tr.json index 15028c4fe65..81f85d4980b 100644 --- a/homeassistant/components/esphome/translations/tr.json +++ b/homeassistant/components/esphome/translations/tr.json @@ -1,8 +1,27 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, "step": { + "authenticate": { + "data": { + "password": "Parola" + }, + "description": "L\u00fctfen yap\u0131land\u0131rman\u0131zda {name} i\u00e7in belirledi\u011finiz parolay\u0131 girin." + }, "discovery_confirm": { "title": "Ke\u015ffedilen ESPHome d\u00fc\u011f\u00fcm\u00fc" + }, + "user": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + } } } } diff --git a/homeassistant/components/esphome/translations/uk.json b/homeassistant/components/esphome/translations/uk.json index d17ec64e548..4643c19cf5d 100644 --- a/homeassistant/components/esphome/translations/uk.json +++ b/homeassistant/components/esphome/translations/uk.json @@ -1,22 +1,25 @@ { "config": { "abort": { - "already_configured": "ESP \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e" + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454." }, "error": { "connection_error": "\u041d\u0435 \u0432\u0434\u0430\u0454\u0442\u044c\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f \u0434\u043e ESP. \u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0444\u0430\u0439\u043b YAML \u043c\u0456\u0441\u0442\u0438\u0442\u044c \u0440\u044f\u0434\u043e\u043a \"api:\".", - "resolve_error": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0430\u0434\u0440\u0435\u0441\u0443 ESP. \u042f\u043a\u0449\u043e \u0446\u044f \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043d\u0435 \u0437\u043d\u0438\u043a\u0430\u0454, \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0456\u0442\u044c \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u0443 IP-\u0430\u0434\u0440\u0435\u0441\u0443: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips" + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "resolve_error": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u0432\u0438\u0437\u043d\u0430\u0447\u0438\u0442\u0438 \u0430\u0434\u0440\u0435\u0441\u0443 ESP. \u042f\u043a\u0449\u043e \u0446\u044f \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u044e\u0454\u0442\u044c\u0441\u044f, \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u0441\u0442\u0430\u0442\u0438\u0447\u043d\u0443 IP-\u0430\u0434\u0440\u0435\u0441\u0443: https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips." }, + "flow_title": "ESPHome: {name}", "step": { "authenticate": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c" }, - "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c, \u044f\u043a\u0438\u0439 \u0432\u0438 \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u043b\u0438 \u0443 \u0441\u0432\u043e\u0457\u0439 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457." + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c, \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0439 \u0432 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457 {name}." }, "discovery_confirm": { "description": "\u0414\u043e\u0434\u0430\u0442\u0438 ESPHome \u0432\u0443\u0437\u043e\u043b {name} \u0443 Home Assistant?", - "title": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u0432\u0443\u0437\u043e\u043b ESPHome" + "title": "ESPHome" }, "user": { "data": { diff --git a/homeassistant/components/fan/translations/tr.json b/homeassistant/components/fan/translations/tr.json index 4ffc57601bd..52a07c35d83 100644 --- a/homeassistant/components/fan/translations/tr.json +++ b/homeassistant/components/fan/translations/tr.json @@ -1,4 +1,14 @@ { + "device_automation": { + "action_type": { + "turn_off": "{entity_name} kapat", + "turn_on": "{entity_name} a\u00e7\u0131n" + }, + "trigger_type": { + "turned_off": "{entity_name} kapat\u0131ld\u0131", + "turned_on": "{entity_name} a\u00e7\u0131ld\u0131" + } + }, "state": { "_": { "off": "Kapal\u0131", diff --git a/homeassistant/components/fan/translations/uk.json b/homeassistant/components/fan/translations/uk.json index 3fd103cd244..0e0bafcbfc4 100644 --- a/homeassistant/components/fan/translations/uk.json +++ b/homeassistant/components/fan/translations/uk.json @@ -1,8 +1,16 @@ { "device_automation": { + "action_type": { + "turn_off": "{entity_name}: \u0432\u0438\u043c\u043a\u043d\u0443\u0442\u0438", + "turn_on": "{entity_name}: \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438" + }, + "condition_type": { + "is_off": "{entity_name} \u0443 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_on": "{entity_name} \u0443 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456" + }, "trigger_type": { - "turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e", - "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e" + "turned_off": "{entity_name} \u0432\u0438\u043c\u0438\u043a\u0430\u0454\u0442\u044c\u0441\u044f", + "turned_on": "{entity_name} \u0432\u043c\u0438\u043a\u0430\u0454\u0442\u044c\u0441\u044f" } }, "state": { diff --git a/homeassistant/components/feedreader/manifest.json b/homeassistant/components/feedreader/manifest.json index 30413d10e43..d1bc9cdb524 100644 --- a/homeassistant/components/feedreader/manifest.json +++ b/homeassistant/components/feedreader/manifest.json @@ -2,6 +2,6 @@ "domain": "feedreader", "name": "Feedreader", "documentation": "https://www.home-assistant.io/integrations/feedreader", - "requirements": ["feedparser-homeassistant==5.2.2.dev1"], + "requirements": ["feedparser==6.0.2"], "codeowners": [] } diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index 97d2b594cc3..d46709924ea 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -12,6 +12,7 @@ import voluptuous as vol from homeassistant.components import history from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.input_number import DOMAIN as INPUT_NUMBER_DOMAIN from homeassistant.components.sensor import ( DEVICE_CLASSES as SENSOR_DEVICE_CLASSES, DOMAIN as SENSOR_DOMAIN, @@ -139,7 +140,9 @@ FILTER_TIME_THROTTLE_SCHEMA = FILTER_SCHEMA.extend( PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_ENTITY_ID): vol.Any( - cv.entity_domain(SENSOR_DOMAIN), cv.entity_domain(BINARY_SENSOR_DOMAIN) + cv.entity_domain(SENSOR_DOMAIN), + cv.entity_domain(BINARY_SENSOR_DOMAIN), + cv.entity_domain(INPUT_NUMBER_DOMAIN), ), vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_FILTERS): vol.All( diff --git a/homeassistant/components/fireservicerota/translations/fr.json b/homeassistant/components/fireservicerota/translations/fr.json new file mode 100644 index 00000000000..a8803f63fca --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/fr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Le compte \u00e0 d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9" + }, + "create_entry": { + "default": "Autentification r\u00e9ussie" + }, + "error": { + "invalid_auth": "Autentification invalide" + }, + "step": { + "reauth": { + "data": { + "password": "Mot de passe" + } + }, + "user": { + "data": { + "password": "Mot de passe", + "url": "Site web", + "username": "Utilisateur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fireservicerota/translations/tr.json b/homeassistant/components/fireservicerota/translations/tr.json index a2d2cab3b74..f54d10f6cbf 100644 --- a/homeassistant/components/fireservicerota/translations/tr.json +++ b/homeassistant/components/fireservicerota/translations/tr.json @@ -1,5 +1,15 @@ { "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "create_entry": { + "default": "Ba\u015far\u0131yla do\u011fruland\u0131" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, "step": { "reauth": { "data": { diff --git a/homeassistant/components/fireservicerota/translations/uk.json b/homeassistant/components/fireservicerota/translations/uk.json new file mode 100644 index 00000000000..2d3bf8c596e --- /dev/null +++ b/homeassistant/components/fireservicerota/translations/uk.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" + }, + "create_entry": { + "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "step": { + "reauth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0422\u043e\u043a\u0435\u043d\u0438 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457 \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0456, \u0443\u0432\u0456\u0439\u0434\u0456\u0442\u044c, \u0449\u043e\u0431 \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u0457\u0445 \u0437\u0430\u043d\u043e\u0432\u043e." + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "url": "\u0412\u0435\u0431-\u0441\u0430\u0439\u0442", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/firmata/translations/tr.json b/homeassistant/components/firmata/translations/tr.json new file mode 100644 index 00000000000..b7d038a229b --- /dev/null +++ b/homeassistant/components/firmata/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/firmata/translations/uk.json b/homeassistant/components/firmata/translations/uk.json new file mode 100644 index 00000000000..41b670fbb18 --- /dev/null +++ b/homeassistant/components/firmata/translations/uk.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/de.json b/homeassistant/components/flick_electric/translations/de.json index ed0ef205ff0..3e3568c45f8 100644 --- a/homeassistant/components/flick_electric/translations/de.json +++ b/homeassistant/components/flick_electric/translations/de.json @@ -4,7 +4,7 @@ "already_configured": "Dieses Konto ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/flick_electric/translations/tr.json b/homeassistant/components/flick_electric/translations/tr.json new file mode 100644 index 00000000000..a83e1936fb4 --- /dev/null +++ b/homeassistant/components/flick_electric/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flick_electric/translations/uk.json b/homeassistant/components/flick_electric/translations/uk.json new file mode 100644 index 00000000000..4d72844bc74 --- /dev/null +++ b/homeassistant/components/flick_electric/translations/uk.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "client_id": "ID \u043a\u043b\u0456\u0454\u043d\u0442\u0430 (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)", + "client_secret": "\u0421\u0435\u043a\u0440\u0435\u0442 \u043a\u043b\u0456\u0454\u043d\u0442\u0430 (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "Flick Electric" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flo/__init__.py b/homeassistant/components/flo/__init__.py index 14d00aa000a..b57cdd5f871 100644 --- a/homeassistant/components/flo/__init__.py +++ b/homeassistant/components/flo/__init__.py @@ -4,7 +4,6 @@ import logging from aioflo import async_get_api from aioflo.errors import RequestError -import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME @@ -15,8 +14,6 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CLIENT, DOMAIN from .device import FloDeviceDataUpdateCoordinator -CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) - _LOGGER = logging.getLogger(__name__) PLATFORMS = ["binary_sensor", "sensor", "switch"] diff --git a/homeassistant/components/flo/translations/de.json b/homeassistant/components/flo/translations/de.json index 38215675701..625c7372347 100644 --- a/homeassistant/components/flo/translations/de.json +++ b/homeassistant/components/flo/translations/de.json @@ -1,12 +1,17 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { "user": { "data": { + "host": "Host", "password": "Passwort", "username": "Benutzername" } diff --git a/homeassistant/components/flo/translations/tr.json b/homeassistant/components/flo/translations/tr.json new file mode 100644 index 00000000000..40c9c39b967 --- /dev/null +++ b/homeassistant/components/flo/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flo/translations/uk.json b/homeassistant/components/flo/translations/uk.json new file mode 100644 index 00000000000..2df11f74455 --- /dev/null +++ b/homeassistant/components/flo/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flume/manifest.json b/homeassistant/components/flume/manifest.json index f00bccb886a..813b8788ed5 100644 --- a/homeassistant/components/flume/manifest.json +++ b/homeassistant/components/flume/manifest.json @@ -4,5 +4,9 @@ "documentation": "https://www.home-assistant.io/integrations/flume/", "requirements": ["pyflume==0.5.5"], "codeowners": ["@ChrisMandich", "@bdraco"], - "config_flow": true + "config_flow": true, + "dhcp": [ + {"hostname":"flume-gw-*","macaddress":"ECFABC*"}, + {"hostname":"flume-gw-*","macaddress":"B4E62D*"} + ] } diff --git a/homeassistant/components/flume/translations/de.json b/homeassistant/components/flume/translations/de.json index 692c38350a8..c38a5593ac7 100644 --- a/homeassistant/components/flume/translations/de.json +++ b/homeassistant/components/flume/translations/de.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Dieses Konto ist bereits konfiguriert" + "already_configured": "Konto wurde bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/flume/translations/tr.json b/homeassistant/components/flume/translations/tr.json new file mode 100644 index 00000000000..a83e1936fb4 --- /dev/null +++ b/homeassistant/components/flume/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flume/translations/uk.json b/homeassistant/components/flume/translations/uk.json new file mode 100644 index 00000000000..53fb4f3d6d7 --- /dev/null +++ b/homeassistant/components/flume/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "client_id": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u043a\u043b\u0456\u0454\u043d\u0442\u0430", + "client_secret": "\u0421\u0435\u043a\u0440\u0435\u0442 \u043a\u043b\u0456\u0454\u043d\u0442\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0429\u043e\u0431 \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u043e\u0433\u043e API Flume, \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 'ID \u043a\u043b\u0456\u0454\u043d\u0442\u0430' \u0456 '\u0421\u0435\u043a\u0440\u0435\u0442 \u043a\u043b\u0456\u0454\u043d\u0442\u0430' \u0437\u0430 \u0430\u0434\u0440\u0435\u0441\u043e\u044e https://portal.flumetech.com/settings#token.", + "title": "Flume" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/de.json b/homeassistant/components/flunearyou/translations/de.json index cd2934170c9..1c94931f405 100644 --- a/homeassistant/components/flunearyou/translations/de.json +++ b/homeassistant/components/flunearyou/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Diese Koordinaten sind bereits registriert." + "already_configured": "Standort ist bereits konfiguriert" }, "error": { "unknown": "Unerwarteter Fehler" diff --git a/homeassistant/components/flunearyou/translations/tr.json b/homeassistant/components/flunearyou/translations/tr.json new file mode 100644 index 00000000000..6e749e3c827 --- /dev/null +++ b/homeassistant/components/flunearyou/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "latitude": "Enlem", + "longitude": "Boylam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/uk.json b/homeassistant/components/flunearyou/translations/uk.json new file mode 100644 index 00000000000..354a04d8e7a --- /dev/null +++ b/homeassistant/components/flunearyou/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435." + }, + "error": { + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430" + }, + "description": "\u041c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0446\u044c\u043a\u0438\u0445 \u0456 CDC \u0437\u0432\u0456\u0442\u0456\u0432 \u0437\u0430 \u0432\u043a\u0430\u0437\u0430\u043d\u0438\u043c\u0438 \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u0430\u043c\u0438.", + "title": "Flu Near You" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/de.json b/homeassistant/components/forked_daapd/translations/de.json index a3cdc53c52a..e90ffc71f90 100644 --- a/homeassistant/components/forked_daapd/translations/de.json +++ b/homeassistant/components/forked_daapd/translations/de.json @@ -5,7 +5,7 @@ }, "error": { "unknown_error": "Unbekannter Fehler", - "wrong_host_or_port": "Verbindung konnte nicht hergestellt werden. Bitte \u00fcberpr\u00fcfen Sie Host und Port.", + "wrong_host_or_port": "Verbindung konnte nicht hergestellt werden. Bitte Host und Port pr\u00fcfen.", "wrong_password": "Ung\u00fcltiges Passwort", "wrong_server_type": "F\u00fcr die forked-daapd Integration ist ein forked-daapd Server mit der Version > = 27.0 erforderlich." }, diff --git a/homeassistant/components/forked_daapd/translations/tr.json b/homeassistant/components/forked_daapd/translations/tr.json new file mode 100644 index 00000000000..cf354c5c87f --- /dev/null +++ b/homeassistant/components/forked_daapd/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "unknown_error": "Beklenmeyen hata", + "wrong_password": "Yanl\u0131\u015f parola." + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "name": "Kolay ad", + "password": "API parolas\u0131 (parola yoksa bo\u015f b\u0131rak\u0131n)", + "port": "API ba\u011flant\u0131 noktas\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/forked_daapd/translations/uk.json b/homeassistant/components/forked_daapd/translations/uk.json new file mode 100644 index 00000000000..19caf9b5bd0 --- /dev/null +++ b/homeassistant/components/forked_daapd/translations/uk.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "not_forked_daapd": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 forked-daapd." + }, + "error": { + "forbidden": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043c\u0435\u0440\u0435\u0436\u0435\u0432\u0456 \u0434\u043e\u0437\u0432\u043e\u043b\u0438 forked-daapd.", + "unknown_error": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430", + "websocket_not_enabled": "\u0412\u0435\u0431-\u0441\u043e\u043a\u0435\u0442 forked-daapd \u043d\u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439.", + "wrong_host_or_port": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f, \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u0445\u043e\u0441\u0442\u0430.", + "wrong_password": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439 \u043f\u0430\u0440\u043e\u043b\u044c.", + "wrong_server_type": "\u041d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u0438\u0439 \u0441\u0435\u0440\u0432\u0435\u0440 forked-daapd \u0432\u0435\u0440\u0441\u0456\u0457 27.0 \u0430\u0431\u043e \u0432\u0438\u0449\u0435." + }, + "flow_title": "\u0421\u0435\u0440\u0432\u0435\u0440 forked-daapd: {name} ({host})", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c API (\u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c, \u044f\u043a\u0449\u043e \u0443 \u0432\u0430\u0441 \u043d\u0435\u043c\u0430\u0454 \u043f\u0430\u0440\u043e\u043b\u044f)", + "port": "\u041f\u043e\u0440\u0442 API" + }, + "title": "forked-daapd" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "librespot_java_port": "\u041f\u043e\u0440\u0442 \u0434\u043b\u044f \u0443\u043f\u0440\u0430\u0432\u043b\u0456\u043d\u043d\u044f \u043a\u0430\u043d\u0430\u043b\u043e\u043c librespot-java (\u044f\u043a\u0449\u043e \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f)", + "max_playlists": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430 \u043a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u043f\u043b\u0435\u0439\u043b\u0438\u0441\u0442\u0456\u0432, \u0449\u043e \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u044e\u0442\u044c\u0441\u044f \u044f\u043a \u0434\u0436\u0435\u0440\u0435\u043b\u0430", + "tts_pause_time": "\u0427\u0430\u0441 \u043f\u0430\u0443\u0437\u0438 \u0434\u043e \u0456 \u043f\u0456\u0441\u043b\u044f TTS (\u0441\u0435\u043a.)", + "tts_volume": "\u0413\u0443\u0447\u043d\u0456\u0441\u0442\u044c TTS (\u0447\u0438\u0441\u043b\u043e \u0432 \u0434\u0456\u0430\u043f\u0430\u0437\u043e\u043d\u0456 \u0432\u0456\u0434 0 \u0434\u043e 1)" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 forked-daapd.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f forked-daapd" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/foscam/__init__.py b/homeassistant/components/foscam/__init__.py index 5c63f7b2a15..e5b82817d4b 100644 --- a/homeassistant/components/foscam/__init__.py +++ b/homeassistant/components/foscam/__init__.py @@ -1 +1,48 @@ """The foscam component.""" +import asyncio + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import DOMAIN, SERVICE_PTZ, SERVICE_PTZ_PRESET + +PLATFORMS = ["camera"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the foscam component.""" + hass.data.setdefault(DOMAIN, {}) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up foscam from a config entry.""" + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + hass.data[DOMAIN][entry.unique_id] = entry.data + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + + if unload_ok: + hass.data[DOMAIN].pop(entry.unique_id) + + if not hass.data[DOMAIN]: + hass.services.async_remove(domain=DOMAIN, service=SERVICE_PTZ) + hass.services.async_remove(domain=DOMAIN, service=SERVICE_PTZ_PRESET) + + return unload_ok diff --git a/homeassistant/components/foscam/camera.py b/homeassistant/components/foscam/camera.py index bc28e160b25..f66ad31c2a8 100644 --- a/homeassistant/components/foscam/camera.py +++ b/homeassistant/components/foscam/camera.py @@ -1,13 +1,13 @@ """This component provides basic support for Foscam IP cameras.""" import asyncio -import logging from libpyfoscam import FoscamCamera import voluptuous as vol from homeassistant.components.camera import PLATFORM_SCHEMA, SUPPORT_STREAM, Camera +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( - ATTR_ENTITY_ID, + CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_PORT, @@ -15,21 +15,18 @@ from homeassistant.const import ( ) from homeassistant.helpers import config_validation as cv, entity_platform -from .const import DATA as FOSCAM_DATA, ENTITIES as FOSCAM_ENTITIES +from .const import CONF_STREAM, DOMAIN, LOGGER, SERVICE_PTZ, SERVICE_PTZ_PRESET -_LOGGER = logging.getLogger(__name__) - -CONF_IP = "ip" -CONF_RTSP_PORT = "rtsp_port" - -DEFAULT_NAME = "Foscam Camera" -DEFAULT_PORT = 88 - -SERVICE_PTZ = "ptz" -ATTR_MOVEMENT = "movement" -ATTR_TRAVELTIME = "travel_time" - -DEFAULT_TRAVELTIME = 0.125 +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required("ip"): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_USERNAME): cv.string, + vol.Optional(CONF_NAME, default="Foscam Camera"): cv.string, + vol.Optional(CONF_PORT, default=88): cv.port, + vol.Optional("rtsp_port"): cv.port, + } +) DIR_UP = "up" DIR_DOWN = "down" @@ -52,43 +49,42 @@ MOVEMENT_ATTRS = { DIR_BOTTOMRIGHT: "ptz_move_bottom_right", } -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_IP): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_RTSP_PORT): cv.port, - } -) +DEFAULT_TRAVELTIME = 0.125 -SERVICE_PTZ_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(ATTR_MOVEMENT): vol.In( - [ - DIR_UP, - DIR_DOWN, - DIR_LEFT, - DIR_RIGHT, - DIR_TOPLEFT, - DIR_TOPRIGHT, - DIR_BOTTOMLEFT, - DIR_BOTTOMRIGHT, - ] - ), - vol.Optional(ATTR_TRAVELTIME, default=DEFAULT_TRAVELTIME): cv.small_float, - } -) +ATTR_MOVEMENT = "movement" +ATTR_TRAVELTIME = "travel_time" +ATTR_PRESET_NAME = "preset_name" + +PTZ_GOTO_PRESET_COMMAND = "ptz_goto_preset" async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up a Foscam IP Camera.""" + LOGGER.warning( + "Loading foscam via platform config is deprecated, it will be automatically imported. Please remove it afterwards." + ) + + config_new = { + CONF_NAME: config[CONF_NAME], + CONF_HOST: config["ip"], + CONF_PORT: config[CONF_PORT], + CONF_USERNAME: config[CONF_USERNAME], + CONF_PASSWORD: config[CONF_PASSWORD], + CONF_STREAM: "Main", + } + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config_new + ) + ) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Add a Foscam IP camera from a config entry.""" platform = entity_platform.current_platform.get() - assert platform is not None platform.async_register_entity_service( - "ptz", + SERVICE_PTZ, { vol.Required(ATTR_MOVEMENT): vol.In( [ @@ -107,61 +103,71 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= "async_perform_ptz", ) + platform.async_register_entity_service( + SERVICE_PTZ_PRESET, + { + vol.Required(ATTR_PRESET_NAME): cv.string, + }, + "async_perform_ptz_preset", + ) + camera = FoscamCamera( - config[CONF_IP], - config[CONF_PORT], - config[CONF_USERNAME], - config[CONF_PASSWORD], + config_entry.data[CONF_HOST], + config_entry.data[CONF_PORT], + config_entry.data[CONF_USERNAME], + config_entry.data[CONF_PASSWORD], verbose=False, ) - rtsp_port = config.get(CONF_RTSP_PORT) - if not rtsp_port: - ret, response = await hass.async_add_executor_job(camera.get_port_info) - - if ret == 0: - rtsp_port = response.get("rtspPort") or response.get("mediaPort") - - ret, response = await hass.async_add_executor_job(camera.get_motion_detect_config) - - motion_status = False - if ret != 0 and response == 1: - motion_status = True - - async_add_entities( - [ - HassFoscamCamera( - camera, - config[CONF_NAME], - config[CONF_USERNAME], - config[CONF_PASSWORD], - rtsp_port, - motion_status, - ) - ] - ) + async_add_entities([HassFoscamCamera(camera, config_entry)]) class HassFoscamCamera(Camera): """An implementation of a Foscam IP camera.""" - def __init__(self, camera, name, username, password, rtsp_port, motion_status): + def __init__(self, camera, config_entry): """Initialize a Foscam camera.""" super().__init__() self._foscam_session = camera - self._name = name - self._username = username - self._password = password - self._rtsp_port = rtsp_port - self._motion_status = motion_status + self._name = config_entry.title + self._username = config_entry.data[CONF_USERNAME] + self._password = config_entry.data[CONF_PASSWORD] + self._stream = config_entry.data[CONF_STREAM] + self._unique_id = config_entry.unique_id + self._rtsp_port = None + self._motion_status = False async def async_added_to_hass(self): """Handle entity addition to hass.""" - entities = self.hass.data.setdefault(FOSCAM_DATA, {}).setdefault( - FOSCAM_ENTITIES, [] + # Get motion detection status + ret, response = await self.hass.async_add_executor_job( + self._foscam_session.get_motion_detect_config ) - entities.append(self) + + if ret != 0: + LOGGER.error( + "Error getting motion detection status of %s: %s", self._name, ret + ) + + else: + self._motion_status = response == 1 + + # Get RTSP port + ret, response = await self.hass.async_add_executor_job( + self._foscam_session.get_port_info + ) + + if ret != 0: + LOGGER.error("Error getting RTSP port of %s: %s", self._name, ret) + + else: + self._rtsp_port = response.get("rtspPort") or response.get("mediaPort") + + @property + def unique_id(self): + """Return the entity unique ID.""" + return self._unique_id def camera_image(self): """Return a still image response from the camera.""" @@ -178,12 +184,14 @@ class HassFoscamCamera(Camera): """Return supported features.""" if self._rtsp_port: return SUPPORT_STREAM - return 0 + + return None async def stream_source(self): """Return the stream source.""" if self._rtsp_port: - return f"rtsp://{self._username}:{self._password}@{self._foscam_session.host}:{self._rtsp_port}/videoMain" + return f"rtsp://{self._username}:{self._password}@{self._foscam_session.host}:{self._rtsp_port}/video{self._stream}" + return None @property @@ -201,7 +209,10 @@ class HassFoscamCamera(Camera): self._motion_status = True except TypeError: - _LOGGER.debug("Communication problem") + LOGGER.debug( + "Failed enabling motion detection on '%s'. Is it supported by the device?", + self._name, + ) def disable_motion_detection(self): """Disable motion detection.""" @@ -213,18 +224,21 @@ class HassFoscamCamera(Camera): self._motion_status = False except TypeError: - _LOGGER.debug("Communication problem") + LOGGER.debug( + "Failed disabling motion detection on '%s'. Is it supported by the device?", + self._name, + ) async def async_perform_ptz(self, movement, travel_time): """Perform a PTZ action on the camera.""" - _LOGGER.debug("PTZ action '%s' on %s", movement, self._name) + LOGGER.debug("PTZ action '%s' on %s", movement, self._name) movement_function = getattr(self._foscam_session, MOVEMENT_ATTRS[movement]) ret, _ = await self.hass.async_add_executor_job(movement_function) if ret != 0: - _LOGGER.error("Error moving %s '%s': %s", movement, self._name, ret) + LOGGER.error("Error moving %s '%s': %s", movement, self._name, ret) return await asyncio.sleep(travel_time) @@ -234,7 +248,21 @@ class HassFoscamCamera(Camera): ) if ret != 0: - _LOGGER.error("Error stopping movement on '%s': %s", self._name, ret) + LOGGER.error("Error stopping movement on '%s': %s", self._name, ret) + return + + async def async_perform_ptz_preset(self, preset_name): + """Perform a PTZ preset action on the camera.""" + LOGGER.debug("PTZ preset '%s' on %s", preset_name, self._name) + + preset_function = getattr(self._foscam_session, PTZ_GOTO_PRESET_COMMAND) + + ret, _ = await self.hass.async_add_executor_job(preset_function, preset_name) + + if ret != 0: + LOGGER.error( + "Error moving to preset %s on '%s': %s", preset_name, self._name, ret + ) return @property diff --git a/homeassistant/components/foscam/config_flow.py b/homeassistant/components/foscam/config_flow.py new file mode 100644 index 00000000000..7bb8cb50a51 --- /dev/null +++ b/homeassistant/components/foscam/config_flow.py @@ -0,0 +1,123 @@ +"""Config flow for foscam integration.""" +from libpyfoscam import FoscamCamera +from libpyfoscam.foscam import ERROR_FOSCAM_AUTH, ERROR_FOSCAM_UNAVAILABLE +import voluptuous as vol + +from homeassistant import config_entries, exceptions +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, +) +from homeassistant.data_entry_flow import AbortFlow + +from .const import CONF_STREAM, LOGGER +from .const import DOMAIN # pylint:disable=unused-import + +STREAMS = ["Main", "Sub"] + +DEFAULT_PORT = 88 + + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_STREAM, default=STREAMS[0]): vol.In(STREAMS), + } +) + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for foscam.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def _validate_and_create(self, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + camera = FoscamCamera( + data[CONF_HOST], + data[CONF_PORT], + data[CONF_USERNAME], + data[CONF_PASSWORD], + verbose=False, + ) + + # Validate data by sending a request to the camera + ret, response = await self.hass.async_add_executor_job(camera.get_dev_info) + + if ret == ERROR_FOSCAM_UNAVAILABLE: + raise CannotConnect + + if ret == ERROR_FOSCAM_AUTH: + raise InvalidAuth + + await self.async_set_unique_id(response["mac"]) + self._abort_if_unique_id_configured() + + name = data.pop(CONF_NAME, response["devName"]) + + return self.async_create_entry(title=name, data=data) + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + + if user_input is not None: + try: + return await self._validate_and_create(user_input) + + except CannotConnect: + errors["base"] = "cannot_connect" + + except InvalidAuth: + errors["base"] = "invalid_auth" + + except AbortFlow: + raise + + except Exception: # pylint: disable=broad-except + LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + async def async_step_import(self, import_config): + """Handle config import from yaml.""" + try: + return await self._validate_and_create(import_config) + + except CannotConnect: + LOGGER.error("Error importing foscam platform config: cannot connect.") + return self.async_abort(reason="cannot_connect") + + except InvalidAuth: + LOGGER.error("Error importing foscam platform config: invalid auth.") + return self.async_abort(reason="invalid_auth") + + except AbortFlow: + raise + + except Exception: # pylint: disable=broad-except + LOGGER.exception( + "Error importing foscam platform config: unexpected exception." + ) + return self.async_abort(reason="unknown") + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/foscam/const.py b/homeassistant/components/foscam/const.py index 63b4b74a763..a42b430993e 100644 --- a/homeassistant/components/foscam/const.py +++ b/homeassistant/components/foscam/const.py @@ -1,5 +1,11 @@ """Constants for Foscam component.""" +import logging + +LOGGER = logging.getLogger(__package__) DOMAIN = "foscam" -DATA = "foscam" -ENTITIES = "entities" + +CONF_STREAM = "stream" + +SERVICE_PTZ = "ptz" +SERVICE_PTZ_PRESET = "ptz_preset" diff --git a/homeassistant/components/foscam/manifest.json b/homeassistant/components/foscam/manifest.json index 8c7e8e7d77a..fdd050d5133 100644 --- a/homeassistant/components/foscam/manifest.json +++ b/homeassistant/components/foscam/manifest.json @@ -1,6 +1,7 @@ { "domain": "foscam", "name": "Foscam", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/foscam", "requirements": ["libpyfoscam==1.0"], "codeowners": ["@skgsergio"] diff --git a/homeassistant/components/foscam/services.yaml b/homeassistant/components/foscam/services.yaml index 33ba82482f1..41563635f68 100644 --- a/homeassistant/components/foscam/services.yaml +++ b/homeassistant/components/foscam/services.yaml @@ -10,3 +10,13 @@ ptz: travel_time: description: "(Optional) Travel time in seconds. Allowed values: float from 0 to 1. Default: 0.125" example: 0.125 + +ptz_preset: + description: PTZ Preset service for Foscam camera. + fields: + entity_id: + description: Name(s) of entities to move. + example: "camera.living_room_camera" + preset_name: + description: "The name of the preset to move to. Presets can be created from within the official Foscam apps." + example: "TopMost" diff --git a/homeassistant/components/foscam/strings.json b/homeassistant/components/foscam/strings.json new file mode 100644 index 00000000000..6033fa099cd --- /dev/null +++ b/homeassistant/components/foscam/strings.json @@ -0,0 +1,24 @@ +{ + "title": "Foscam", + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "stream": "Stream" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/foscam/translations/af.json b/homeassistant/components/foscam/translations/af.json new file mode 100644 index 00000000000..4a9930dd95d --- /dev/null +++ b/homeassistant/components/foscam/translations/af.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Senha" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/ca.json b/homeassistant/components/foscam/translations/ca.json new file mode 100644 index 00000000000..5a6c84f400e --- /dev/null +++ b/homeassistant/components/foscam/translations/ca.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "password": "Contrasenya", + "port": "Port", + "stream": "Flux de v\u00eddeo", + "username": "Nom d'usuari" + } + } + } + }, + "title": "Foscam" +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/cs.json b/homeassistant/components/foscam/translations/cs.json new file mode 100644 index 00000000000..b6f3c40abf6 --- /dev/null +++ b/homeassistant/components/foscam/translations/cs.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "port": "Port", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + }, + "title": "Foscam" +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/de.json b/homeassistant/components/foscam/translations/de.json new file mode 100644 index 00000000000..603be1847cc --- /dev/null +++ b/homeassistant/components/foscam/translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Passwort", + "port": "Port", + "username": "Benutzername" + } + } + } + }, + "title": "Foscam" +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/en.json b/homeassistant/components/foscam/translations/en.json new file mode 100644 index 00000000000..3d1454a4ebd --- /dev/null +++ b/homeassistant/components/foscam/translations/en.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "port": "Port", + "stream": "Stream", + "username": "Username" + } + } + } + }, + "title": "Foscam" +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/es.json b/homeassistant/components/foscam/translations/es.json new file mode 100644 index 00000000000..27f7ac36489 --- /dev/null +++ b/homeassistant/components/foscam/translations/es.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a", + "port": "Puerto", + "stream": "Stream", + "username": "Usuario" + } + } + } + }, + "title": "Foscam" +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/et.json b/homeassistant/components/foscam/translations/et.json new file mode 100644 index 00000000000..b20a33aec1d --- /dev/null +++ b/homeassistant/components/foscam/translations/et.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Vigane autentimine", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Salas\u00f5na", + "port": "Port", + "stream": "Voog", + "username": "Kasutajanimi" + } + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/fr.json b/homeassistant/components/foscam/translations/fr.json new file mode 100644 index 00000000000..9af8115c305 --- /dev/null +++ b/homeassistant/components/foscam/translations/fr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Le p\u00e9riph\u00e9rique est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "Echec de connection", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te", + "password": "Mot de passe", + "port": "Port", + "stream": "Flux", + "username": "Nom d'utilisateur" + } + } + } + }, + "title": "Foscam" +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/it.json b/homeassistant/components/foscam/translations/it.json new file mode 100644 index 00000000000..0562012b1fa --- /dev/null +++ b/homeassistant/components/foscam/translations/it.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host", + "password": "Password", + "port": "Porta", + "stream": "Flusso", + "username": "Nome utente" + } + } + } + }, + "title": "Foscam" +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/lb.json b/homeassistant/components/foscam/translations/lb.json new file mode 100644 index 00000000000..123b3f4be76 --- /dev/null +++ b/homeassistant/components/foscam/translations/lb.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Host", + "password": "Passwuert", + "port": "Port", + "stream": "Stream", + "username": "Benotzernumm" + } + } + } + }, + "title": "Foscam" +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/no.json b/homeassistant/components/foscam/translations/no.json new file mode 100644 index 00000000000..5e1b494c88a --- /dev/null +++ b/homeassistant/components/foscam/translations/no.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "password": "Passord", + "port": "Port", + "stream": "Str\u00f8m", + "username": "Brukernavn" + } + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/pl.json b/homeassistant/components/foscam/translations/pl.json new file mode 100644 index 00000000000..ef0bcda2b3a --- /dev/null +++ b/homeassistant/components/foscam/translations/pl.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o", + "port": "Port", + "stream": "Strumie\u0144", + "username": "Nazwa u\u017cytkownika" + } + } + } + }, + "title": "Foscam" +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/pt.json b/homeassistant/components/foscam/translations/pt.json new file mode 100644 index 00000000000..b8a454fbaba --- /dev/null +++ b/homeassistant/components/foscam/translations/pt.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Palavra-passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/ru.json b/homeassistant/components/foscam/translations/ru.json new file mode 100644 index 00000000000..ad8b7961ca3 --- /dev/null +++ b/homeassistant/components/foscam/translations/ru.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "stream": "\u041f\u043e\u0442\u043e\u043a", + "username": "\u041b\u043e\u0433\u0438\u043d" + } + } + } + }, + "title": "Foscam" +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/tr.json b/homeassistant/components/foscam/translations/tr.json new file mode 100644 index 00000000000..b3e964ae08e --- /dev/null +++ b/homeassistant/components/foscam/translations/tr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen Hata" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "password": "\u015eifre", + "port": "Port", + "stream": "Ak\u0131\u015f", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "title": "Foscam" +} \ No newline at end of file diff --git a/homeassistant/components/foscam/translations/zh-Hant.json b/homeassistant/components/foscam/translations/zh-Hant.json new file mode 100644 index 00000000000..2cc6303c17a --- /dev/null +++ b/homeassistant/components/foscam/translations/zh-Hant.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "stream": "\u4e32\u6d41", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + }, + "title": "Foscam" +} \ No newline at end of file diff --git a/homeassistant/components/freebox/translations/de.json b/homeassistant/components/freebox/translations/de.json index c21e3c6b67f..738b9d48f3c 100644 --- a/homeassistant/components/freebox/translations/de.json +++ b/homeassistant/components/freebox/translations/de.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "already_configured": "Host bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "register_failed": "Registrieren fehlgeschlagen, bitte versuche es erneut", - "unknown": "Unbekannter Fehler: Bitte versuchen Sie es sp\u00e4ter erneut" + "unknown": "Unerwarteter Fehler" }, "step": { "link": { diff --git a/homeassistant/components/freebox/translations/tr.json b/homeassistant/components/freebox/translations/tr.json new file mode 100644 index 00000000000..b675d38057d --- /dev/null +++ b/homeassistant/components/freebox/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + }, + "title": "Freebox" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/translations/uk.json b/homeassistant/components/freebox/translations/uk.json new file mode 100644 index 00000000000..8676c9164a1 --- /dev/null +++ b/homeassistant/components/freebox/translations/uk.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "register_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0432\u0430\u0442\u0438\u0441\u044f, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "link": { + "description": "\u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c '\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0438', \u043f\u043e\u0442\u0456\u043c \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u043a\u043d\u043e\u043f\u043a\u0443 \u0437\u0456 \u0441\u0442\u0440\u0456\u043b\u043a\u043e\u044e \u043d\u0430 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0456, \u0449\u043e\u0431 \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0432\u0430\u0442\u0438 Freebox \u0432 Home Assistant. \n\n![\u0420\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f \u043a\u043d\u043e\u043f\u043a\u0438 \u043d\u0430 \u0440\u043e\u0443\u0442\u0435\u0440\u0456] (/ static / images / config_freebox.png)", + "title": "Freebox" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "Freebox" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/ca.json b/homeassistant/components/fritzbox/translations/ca.json index 8b0122dbe18..f8550b5bc32 100644 --- a/homeassistant/components/fritzbox/translations/ca.json +++ b/homeassistant/components/fritzbox/translations/ca.json @@ -4,7 +4,8 @@ "already_configured": "El dispositiu ja est\u00e0 configurat", "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "no_devices_found": "No s'han trobat dispositius a la xarxa", - "not_supported": "Connectat a AVM FRITZ!Box per\u00f2 no es poden controlar dispositius Smart Home." + "not_supported": "Connectat a AVM FRITZ!Box per\u00f2 no es poden controlar dispositius Smart Home.", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" @@ -18,6 +19,13 @@ }, "description": "Vols configurar {name}?" }, + "reauth_confirm": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "description": "Actualitza la informaci\u00f3 d'inici de sessi\u00f3 de {name}." + }, "user": { "data": { "host": "Amfitri\u00f3", diff --git a/homeassistant/components/fritzbox/translations/cs.json b/homeassistant/components/fritzbox/translations/cs.json index 67ff5db7f99..b3b41afe383 100644 --- a/homeassistant/components/fritzbox/translations/cs.json +++ b/homeassistant/components/fritzbox/translations/cs.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", - "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" @@ -17,6 +18,12 @@ }, "description": "Chcete nastavit {name}?" }, + "reauth_confirm": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + }, "user": { "data": { "host": "Hostitel", diff --git a/homeassistant/components/fritzbox/translations/de.json b/homeassistant/components/fritzbox/translations/de.json index 19ca2e80903..9b76ad19ff4 100644 --- a/homeassistant/components/fritzbox/translations/de.json +++ b/homeassistant/components/fritzbox/translations/de.json @@ -1,10 +1,14 @@ { "config": { "abort": { - "already_configured": "Diese AVM FRITZ! Box ist bereits konfiguriert.", - "already_in_progress": "Die Konfiguration der AVM FRITZ! Box ist bereits in Bearbeitung.", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", "not_supported": "Verbunden mit AVM FRITZ! Box, kann jedoch keine Smart Home-Ger\u00e4te steuern." }, + "error": { + "invalid_auth": "Ung\u00fcltige Zugangsdaten" + }, "flow_title": "AVM FRITZ! Box: {name}", "step": { "confirm": { @@ -12,7 +16,7 @@ "password": "Passwort", "username": "Benutzername" }, - "description": "M\u00f6chten Sie {name} einrichten?" + "description": "M\u00f6chtest du {name} einrichten?" }, "user": { "data": { @@ -20,7 +24,7 @@ "password": "Passwort", "username": "Benutzername" }, - "description": "Geben Sie Ihre AVM FRITZ! Box-Informationen ein." + "description": "Gib deine AVM FRITZ! Box-Informationen ein." } } } diff --git a/homeassistant/components/fritzbox/translations/en.json b/homeassistant/components/fritzbox/translations/en.json index 1f22bc30252..61ca1e957bb 100644 --- a/homeassistant/components/fritzbox/translations/en.json +++ b/homeassistant/components/fritzbox/translations/en.json @@ -4,7 +4,8 @@ "already_configured": "Device is already configured", "already_in_progress": "Configuration flow is already in progress", "no_devices_found": "No devices found on the network", - "not_supported": "Connected to AVM FRITZ!Box but it's unable to control Smart Home devices." + "not_supported": "Connected to AVM FRITZ!Box but it's unable to control Smart Home devices.", + "reauth_successful": "Re-authentication was successful" }, "error": { "invalid_auth": "Invalid authentication" @@ -18,6 +19,13 @@ }, "description": "Do you want to set up {name}?" }, + "reauth_confirm": { + "data": { + "password": "Password", + "username": "Username" + }, + "description": "Update your login information for {name}." + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/fritzbox/translations/et.json b/homeassistant/components/fritzbox/translations/et.json index 702488bce0c..5ee2dc801f4 100644 --- a/homeassistant/components/fritzbox/translations/et.json +++ b/homeassistant/components/fritzbox/translations/et.json @@ -4,7 +4,8 @@ "already_configured": "Seade on juba h\u00e4\u00e4lestatud", "already_in_progress": "Seadistamine on juba k\u00e4imas", "no_devices_found": "V\u00f5rgust ei leitud seadmeid", - "not_supported": "\u00dchendatud AVM FRITZ!Boxiga! kuid see ei saa juhtida Smart Home seadmeid." + "not_supported": "\u00dchendatud AVM FRITZ!Boxiga! kuid see ei saa juhtida Smart Home seadmeid.", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "invalid_auth": "Tuvastamise viga" @@ -18,6 +19,13 @@ }, "description": "Kas soovid seadistada {name}?" }, + "reauth_confirm": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + }, + "description": "V\u00e4rskenda konto {name} sisselogimisteavet." + }, "user": { "data": { "host": "", diff --git a/homeassistant/components/fritzbox/translations/it.json b/homeassistant/components/fritzbox/translations/it.json index ab44ae13863..a420b3f6de7 100644 --- a/homeassistant/components/fritzbox/translations/it.json +++ b/homeassistant/components/fritzbox/translations/it.json @@ -4,7 +4,8 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "no_devices_found": "Nessun dispositivo trovato sulla rete", - "not_supported": "Collegato a AVM FRITZ!Box ma non \u00e8 in grado di controllare i dispositivi Smart Home." + "not_supported": "Collegato a AVM FRITZ!Box ma non \u00e8 in grado di controllare i dispositivi Smart Home.", + "reauth_successful": "La riautenticazione ha avuto successo" }, "error": { "invalid_auth": "Autenticazione non valida" @@ -18,6 +19,13 @@ }, "description": "Vuoi impostare {name}?" }, + "reauth_confirm": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "description": "Aggiorna le tue informazioni di accesso per {name}." + }, "user": { "data": { "host": "Host", diff --git a/homeassistant/components/fritzbox/translations/no.json b/homeassistant/components/fritzbox/translations/no.json index 024e25741a7..bd64b428bdf 100644 --- a/homeassistant/components/fritzbox/translations/no.json +++ b/homeassistant/components/fritzbox/translations/no.json @@ -4,7 +4,8 @@ "already_configured": "Enheten er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket", - "not_supported": "Tilkoblet AVM FRITZ! Box, men den klarer ikke \u00e5 kontrollere Smart Home-enheter." + "not_supported": "Tilkoblet AVM FRITZ! Box, men den klarer ikke \u00e5 kontrollere Smart Home-enheter.", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "invalid_auth": "Ugyldig godkjenning" @@ -18,6 +19,13 @@ }, "description": "Vil du sette opp {name} ?" }, + "reauth_confirm": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "description": "Oppdater p\u00e5loggingsinformasjonen for {name} ." + }, "user": { "data": { "host": "Vert", diff --git a/homeassistant/components/fritzbox/translations/pl.json b/homeassistant/components/fritzbox/translations/pl.json index fc162310189..dc05e431832 100644 --- a/homeassistant/components/fritzbox/translations/pl.json +++ b/homeassistant/components/fritzbox/translations/pl.json @@ -4,7 +4,8 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", "already_in_progress": "Konfiguracja jest ju\u017c w toku", "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci", - "not_supported": "Po\u0142\u0105czony z AVM FRITZ!Box, ale nie jest w stanie kontrolowa\u0107 urz\u0105dze\u0144 Smart Home" + "not_supported": "Po\u0142\u0105czony z AVM FRITZ!Box, ale nie jest w stanie kontrolowa\u0107 urz\u0105dze\u0144 Smart Home", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "invalid_auth": "Niepoprawne uwierzytelnienie" @@ -18,6 +19,13 @@ }, "description": "Czy chcesz skonfigurowa\u0107 {name}?" }, + "reauth_confirm": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Zaktualizuj dane logowania dla {name}" + }, "user": { "data": { "host": "Nazwa hosta lub adres IP", diff --git a/homeassistant/components/fritzbox/translations/ru.json b/homeassistant/components/fritzbox/translations/ru.json index 322f677c2af..50146b490ba 100644 --- a/homeassistant/components/fritzbox/translations/ru.json +++ b/homeassistant/components/fritzbox/translations/ru.json @@ -4,7 +4,8 @@ "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438.", - "not_supported": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a AVM FRITZ! Box \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043e, \u043d\u043e \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c\u0438 Smart Home \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e." + "not_supported": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a AVM FRITZ! Box \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043e, \u043d\u043e \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430\u043c\u0438 Smart Home \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." @@ -18,6 +19,13 @@ }, "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?" }, + "reauth_confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "description": "\u041e\u0431\u043d\u043e\u0432\u0438\u0442\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0443\u044e \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u0434\u043b\u044f {name}." + }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", diff --git a/homeassistant/components/fritzbox/translations/tr.json b/homeassistant/components/fritzbox/translations/tr.json new file mode 100644 index 00000000000..746fe594e19 --- /dev/null +++ b/homeassistant/components/fritzbox/translations/tr.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "confirm": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "{name} kurmak istiyor musunuz?" + }, + "reauth_confirm": { + "data": { + "password": "\u015eifre", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "Giri\u015f bilgilerinizi {name} i\u00e7in g\u00fcncelleyin." + }, + "user": { + "data": { + "host": "Ana Bilgisayar", + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/uk.json b/homeassistant/components/fritzbox/translations/uk.json new file mode 100644 index 00000000000..5a2d8a1c35e --- /dev/null +++ b/homeassistant/components/fritzbox/translations/uk.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "not_supported": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e AVM FRITZ! Box \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u043e, \u0430\u043b\u0435 \u0443\u043f\u0440\u0430\u0432\u043b\u0456\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044f\u043c\u0438 Smart Home \u043d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "flow_title": "AVM FRITZ!Box: {name}", + "step": { + "confirm": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name}?" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 AVM FRITZ! Box." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/zh-Hant.json b/homeassistant/components/fritzbox/translations/zh-Hant.json index 7b85df577ef..71a74785267 100644 --- a/homeassistant/components/fritzbox/translations/zh-Hant.json +++ b/homeassistant/components/fritzbox/translations/zh-Hant.json @@ -4,7 +4,8 @@ "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e", - "not_supported": "\u5df2\u9023\u7dda\u81f3 AVM FRITZ!Box \u4f46\u7121\u6cd5\u63a7\u5236\u667a\u80fd\u5bb6\u5ead\u88dd\u7f6e\u3002" + "not_supported": "\u5df2\u9023\u7dda\u81f3 AVM FRITZ!Box \u4f46\u7121\u6cd5\u63a7\u5236\u667a\u80fd\u5bb6\u5ead\u88dd\u7f6e\u3002", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" @@ -18,6 +19,13 @@ }, "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f" }, + "reauth_confirm": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u66f4\u65b0 {name} \u767b\u5165\u8cc7\u8a0a\u3002" + }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", diff --git a/homeassistant/components/fritzbox_callmonitor/__init__.py b/homeassistant/components/fritzbox_callmonitor/__init__.py index f9a52021606..933dd797dfc 100644 --- a/homeassistant/components/fritzbox_callmonitor/__init__.py +++ b/homeassistant/components/fritzbox_callmonitor/__init__.py @@ -1 +1,92 @@ -"""The fritzbox_callmonitor component.""" +"""The fritzbox_callmonitor integration.""" +from asyncio import gather +import logging + +from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError +from requests.exceptions import ConnectionError as RequestsConnectionError + +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.exceptions import ConfigEntryNotReady + +from .base import FritzBoxPhonebook +from .const import ( + CONF_PHONEBOOK, + CONF_PREFIXES, + DOMAIN, + FRITZBOX_PHONEBOOK, + PLATFORMS, + UNDO_UPDATE_LISTENER, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass, config): + """Set up the fritzbox_callmonitor integration.""" + return True + + +async def async_setup_entry(hass, config_entry): + """Set up the fritzbox_callmonitor platforms.""" + fritzbox_phonebook = FritzBoxPhonebook( + host=config_entry.data[CONF_HOST], + username=config_entry.data[CONF_USERNAME], + password=config_entry.data[CONF_PASSWORD], + phonebook_id=config_entry.data[CONF_PHONEBOOK], + prefixes=config_entry.options.get(CONF_PREFIXES), + ) + + try: + await hass.async_add_executor_job(fritzbox_phonebook.init_phonebook) + except FritzSecurityError as ex: + _LOGGER.error( + "User has insufficient permissions to access AVM FRITZ!Box settings and its phonebooks: %s", + ex, + ) + return False + except FritzConnectionException as ex: + _LOGGER.error("Invalid authentication: %s", ex) + return False + except RequestsConnectionError as ex: + _LOGGER.error("Unable to connect to AVM FRITZ!Box call monitor: %s", ex) + raise ConfigEntryNotReady from ex + + undo_listener = config_entry.add_update_listener(update_listener) + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][config_entry.entry_id] = { + FRITZBOX_PHONEBOOK: fritzbox_phonebook, + UNDO_UPDATE_LISTENER: undo_listener, + } + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, component) + ) + + return True + + +async def async_unload_entry(hass, config_entry): + """Unloading the fritzbox_callmonitor platforms.""" + + unload_ok = all( + await gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in PLATFORMS + ] + ) + ) + + hass.data[DOMAIN][config_entry.entry_id][UNDO_UPDATE_LISTENER]() + + if unload_ok: + hass.data[DOMAIN].pop(config_entry.entry_id) + + return unload_ok + + +async def update_listener(hass, config_entry): + """Update listener to reload after option has changed.""" + await hass.config_entries.async_reload(config_entry.entry_id) diff --git a/homeassistant/components/fritzbox_callmonitor/base.py b/homeassistant/components/fritzbox_callmonitor/base.py new file mode 100644 index 00000000000..79f82de95b7 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/base.py @@ -0,0 +1,79 @@ +"""Base class for fritzbox_callmonitor entities.""" +from datetime import timedelta +import logging +import re + +from fritzconnection.lib.fritzphonebook import FritzPhonebook + +from homeassistant.util import Throttle + +from .const import REGEX_NUMBER, UNKOWN_NAME + +_LOGGER = logging.getLogger(__name__) + +# Return cached results if phonebook was downloaded less then this time ago. +MIN_TIME_PHONEBOOK_UPDATE = timedelta(hours=6) + + +class FritzBoxPhonebook: + """This connects to a FritzBox router and downloads its phone book.""" + + def __init__(self, host, username, password, phonebook_id, prefixes): + """Initialize the class.""" + self.host = host + self.username = username + self.password = password + self.phonebook_id = phonebook_id + self.phonebook_dict = None + self.number_dict = None + self.prefixes = prefixes + self.fph = None + + def init_phonebook(self): + """Establish a connection to the FRITZ!Box and check if phonebook_id is valid.""" + self.fph = FritzPhonebook( + address=self.host, + user=self.username, + password=self.password, + ) + self.update_phonebook() + + @Throttle(MIN_TIME_PHONEBOOK_UPDATE) + def update_phonebook(self): + """Update the phone book dictionary.""" + if not self.phonebook_id: + return + + self.phonebook_dict = self.fph.get_all_names(self.phonebook_id) + self.number_dict = { + re.sub(REGEX_NUMBER, "", nr): name + for name, nrs in self.phonebook_dict.items() + for nr in nrs + } + _LOGGER.info("Fritz!Box phone book successfully updated") + + def get_phonebook_ids(self): + """Return list of phonebook ids.""" + return self.fph.phonebook_ids + + def get_name(self, number): + """Return a name for a given phone number.""" + number = re.sub(REGEX_NUMBER, "", str(number)) + if self.number_dict is None: + return UNKOWN_NAME + + if number in self.number_dict: + return self.number_dict[number] + + if not self.prefixes: + return UNKOWN_NAME + + for prefix in self.prefixes: + try: + return self.number_dict[prefix + number] + except KeyError: + pass + try: + return self.number_dict[prefix + number.lstrip("0")] + except KeyError: + pass diff --git a/homeassistant/components/fritzbox_callmonitor/config_flow.py b/homeassistant/components/fritzbox_callmonitor/config_flow.py new file mode 100644 index 00000000000..ab296c84121 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/config_flow.py @@ -0,0 +1,265 @@ +"""Config flow for fritzbox_callmonitor.""" + +from fritzconnection import FritzConnection +from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError +from requests.exceptions import ConnectionError as RequestsConnectionError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, +) +from homeassistant.core import callback + +from .base import FritzBoxPhonebook + +# pylint:disable=unused-import +from .const import ( + CONF_PHONEBOOK, + CONF_PREFIXES, + DEFAULT_HOST, + DEFAULT_PHONEBOOK, + DEFAULT_PORT, + DEFAULT_USERNAME, + DOMAIN, + FRITZ_ACTION_GET_INFO, + FRITZ_ATTR_NAME, + FRITZ_ATTR_SERIAL_NUMBER, + FRITZ_SERVICE_DEVICE_INFO, + SERIAL_NUMBER, +) + +DATA_SCHEMA_USER = vol.Schema( + { + vol.Required(CONF_HOST, default=DEFAULT_HOST): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): vol.Coerce(int), + vol.Required(CONF_USERNAME, default=DEFAULT_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) + +RESULT_INVALID_AUTH = "invalid_auth" +RESULT_INSUFFICIENT_PERMISSIONS = "insufficient_permissions" +RESULT_MALFORMED_PREFIXES = "malformed_prefixes" +RESULT_NO_DEVIES_FOUND = "no_devices_found" +RESULT_SUCCESS = "success" + + +class FritzBoxCallMonitorConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a fritzbox_callmonitor config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize flow.""" + self._host = None + self._port = None + self._username = None + self._password = None + self._phonebook_name = None + self._phonebook_names = None + self._phonebook_id = None + self._phonebook_ids = None + self._fritzbox_phonebook = None + self._prefixes = None + self._serial_number = None + + def _get_config_entry(self): + """Create and return an config entry.""" + return self.async_create_entry( + title=self._phonebook_name, + data={ + CONF_HOST: self._host, + CONF_PORT: self._port, + CONF_USERNAME: self._username, + CONF_PASSWORD: self._password, + CONF_PHONEBOOK: self._phonebook_id, + CONF_PREFIXES: self._prefixes, + SERIAL_NUMBER: self._serial_number, + }, + ) + + def _try_connect(self): + """Try to connect and check auth.""" + self._fritzbox_phonebook = FritzBoxPhonebook( + host=self._host, + username=self._username, + password=self._password, + phonebook_id=self._phonebook_id, + prefixes=self._prefixes, + ) + + try: + self._fritzbox_phonebook.init_phonebook() + self._phonebook_ids = self._fritzbox_phonebook.get_phonebook_ids() + + fritz_connection = FritzConnection( + address=self._host, user=self._username, password=self._password + ) + device_info = fritz_connection.call_action( + FRITZ_SERVICE_DEVICE_INFO, FRITZ_ACTION_GET_INFO + ) + self._serial_number = device_info[FRITZ_ATTR_SERIAL_NUMBER] + + return RESULT_SUCCESS + except RequestsConnectionError: + return RESULT_NO_DEVIES_FOUND + except FritzSecurityError: + return RESULT_INSUFFICIENT_PERMISSIONS + except FritzConnectionException: + return RESULT_INVALID_AUTH + + async def _get_name_of_phonebook(self, phonebook_id): + """Return name of phonebook for given phonebook_id.""" + phonebook_info = await self.hass.async_add_executor_job( + self._fritzbox_phonebook.fph.phonebook_info, phonebook_id + ) + return phonebook_info[FRITZ_ATTR_NAME] + + async def _get_list_of_phonebook_names(self): + """Return list of names for all available phonebooks.""" + phonebook_names = [] + + for phonebook_id in self._phonebook_ids: + phonebook_names.append(await self._get_name_of_phonebook(phonebook_id)) + + return phonebook_names + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return FritzBoxCallMonitorOptionsFlowHandler(config_entry) + + async def async_step_import(self, user_input=None): + """Handle configuration by yaml file.""" + return await self.async_step_user(user_input) + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + + if user_input is None: + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA_USER, errors={} + ) + + self._host = user_input[CONF_HOST] + self._port = user_input[CONF_PORT] + self._password = user_input[CONF_PASSWORD] + self._username = user_input[CONF_USERNAME] + + result = await self.hass.async_add_executor_job(self._try_connect) + + if result == RESULT_INVALID_AUTH: + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA_USER, + errors={"base": RESULT_INVALID_AUTH}, + ) + + if result != RESULT_SUCCESS: + return self.async_abort(reason=result) + + if ( # pylint: disable=no-member + self.context["source"] == config_entries.SOURCE_IMPORT + ): + self._phonebook_id = user_input[CONF_PHONEBOOK] + self._phonebook_name = user_input[CONF_NAME] + + elif len(self._phonebook_ids) > 1: + return await self.async_step_phonebook() + + else: + self._phonebook_id = DEFAULT_PHONEBOOK + self._phonebook_name = await self._get_name_of_phonebook(self._phonebook_id) + + await self.async_set_unique_id(f"{self._serial_number}-{self._phonebook_id}") + self._abort_if_unique_id_configured() + + return self._get_config_entry() + + async def async_step_phonebook(self, user_input=None): + """Handle a flow to chose one of multiple available phonebooks.""" + + if self._phonebook_names is None: + self._phonebook_names = await self._get_list_of_phonebook_names() + + if user_input is None: + return self.async_show_form( + step_id="phonebook", + data_schema=vol.Schema( + {vol.Required(CONF_PHONEBOOK): vol.In(self._phonebook_names)} + ), + errors={}, + ) + + self._phonebook_name = user_input[CONF_PHONEBOOK] + self._phonebook_id = self._phonebook_names.index(self._phonebook_name) + + await self.async_set_unique_id(f"{self._serial_number}-{self._phonebook_id}") + self._abort_if_unique_id_configured() + + return self._get_config_entry() + + +class FritzBoxCallMonitorOptionsFlowHandler(config_entries.OptionsFlow): + """Handle a fritzbox_callmonitor options flow.""" + + def __init__(self, config_entry): + """Initialize.""" + self.config_entry = config_entry + + @classmethod + def _are_prefixes_valid(cls, prefixes): + """Check if prefixes are valid.""" + return prefixes.strip() if prefixes else prefixes is None + + @classmethod + def _get_list_of_prefixes(cls, prefixes): + """Get list of prefixes.""" + if prefixes is None: + return None + return [prefix.strip() for prefix in prefixes.split(",")] + + def _get_option_schema_prefixes(self): + """Get option schema for entering prefixes.""" + return vol.Schema( + { + vol.Optional( + CONF_PREFIXES, + description={ + "suggested_value": self.config_entry.options.get(CONF_PREFIXES) + }, + ): str + } + ) + + async def async_step_init(self, user_input=None): + """Manage the options.""" + + option_schema_prefixes = self._get_option_schema_prefixes() + + if user_input is None: + return self.async_show_form( + step_id="init", + data_schema=option_schema_prefixes, + errors={}, + ) + + prefixes = user_input.get(CONF_PREFIXES) + + if not self._are_prefixes_valid(prefixes): + return self.async_show_form( + step_id="init", + data_schema=option_schema_prefixes, + errors={"base": RESULT_MALFORMED_PREFIXES}, + ) + + return self.async_create_entry( + title="", data={CONF_PREFIXES: self._get_list_of_prefixes(prefixes)} + ) diff --git a/homeassistant/components/fritzbox_callmonitor/const.py b/homeassistant/components/fritzbox_callmonitor/const.py new file mode 100644 index 00000000000..a71f14401b3 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/const.py @@ -0,0 +1,41 @@ +"""Constants for the AVM Fritz!Box call monitor integration.""" + +STATE_RINGING = "ringing" +STATE_DIALING = "dialing" +STATE_TALKING = "talking" +STATE_IDLE = "idle" + +FRITZ_STATE_RING = "RING" +FRITZ_STATE_CALL = "CALL" +FRITZ_STATE_CONNECT = "CONNECT" +FRITZ_STATE_DISCONNECT = "DISCONNECT" + +ICON_PHONE = "mdi:phone" + +ATTR_PREFIXES = "prefixes" + +FRITZ_ACTION_GET_INFO = "GetInfo" +FRITZ_ATTR_NAME = "name" +FRITZ_ATTR_SERIAL_NUMBER = "NewSerialNumber" +FRITZ_SERVICE_DEVICE_INFO = "DeviceInfo" + +UNKOWN_NAME = "unknown" +SERIAL_NUMBER = "serial_number" +REGEX_NUMBER = r"[^\d\+]" + +CONF_PHONEBOOK = "phonebook" +CONF_PHONEBOOK_NAME = "phonebook_name" +CONF_PREFIXES = "prefixes" + +DEFAULT_HOST = "169.254.1.1" # IP valid for all Fritz!Box routers +DEFAULT_PORT = 1012 +DEFAULT_USERNAME = "admin" +DEFAULT_PHONEBOOK = 0 +DEFAULT_NAME = "Phone" + +DOMAIN = "fritzbox_callmonitor" +MANUFACTURER = "AVM" + +PLATFORMS = ["sensor"] +UNDO_UPDATE_LISTENER = "undo_update_listener" +FRITZBOX_PHONEBOOK = "fritzbox_phonebook" diff --git a/homeassistant/components/fritzbox_callmonitor/manifest.json b/homeassistant/components/fritzbox_callmonitor/manifest.json index 4879842ee22..256292c88f7 100644 --- a/homeassistant/components/fritzbox_callmonitor/manifest.json +++ b/homeassistant/components/fritzbox_callmonitor/manifest.json @@ -1,6 +1,7 @@ { "domain": "fritzbox_callmonitor", "name": "AVM FRITZ!Box Call Monitor", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/fritzbox_callmonitor", "requirements": ["fritzconnection==1.4.0"], "codeowners": [] diff --git a/homeassistant/components/fritzbox_callmonitor/sensor.py b/homeassistant/components/fritzbox_callmonitor/sensor.py index 2656e07c3a5..891bf8131d6 100644 --- a/homeassistant/components/fritzbox_callmonitor/sensor.py +++ b/homeassistant/components/fritzbox_callmonitor/sensor.py @@ -1,15 +1,15 @@ """Sensor to monitor incoming/outgoing phone calls on a Fritz!Box router.""" -import datetime +from datetime import datetime, timedelta import logging -import re -import socket -import threading -import time +import queue +from threading import Event as ThreadingEvent, Thread +from time import sleep -from fritzconnection.lib.fritzphonebook import FritzPhonebook +from fritzconnection.core.fritzmonitor import FritzMonitor import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -20,97 +20,125 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle + +from .const import ( + ATTR_PREFIXES, + CONF_PHONEBOOK, + CONF_PREFIXES, + DEFAULT_HOST, + DEFAULT_NAME, + DEFAULT_PHONEBOOK, + DEFAULT_PORT, + DEFAULT_USERNAME, + DOMAIN, + FRITZ_STATE_CALL, + FRITZ_STATE_CONNECT, + FRITZ_STATE_DISCONNECT, + FRITZ_STATE_RING, + FRITZBOX_PHONEBOOK, + ICON_PHONE, + MANUFACTURER, + SERIAL_NUMBER, + STATE_DIALING, + STATE_IDLE, + STATE_RINGING, + STATE_TALKING, + UNKOWN_NAME, +) _LOGGER = logging.getLogger(__name__) -CONF_PHONEBOOK = "phonebook" -CONF_PREFIXES = "prefixes" - -DEFAULT_HOST = "169.254.1.1" # IP valid for all Fritz!Box routers -DEFAULT_USERNAME = "admin" -DEFAULT_NAME = "Phone" -DEFAULT_PORT = 1012 -DEFAULT_PHONEBOOK = 0 - -INTERVAL_RECONNECT = 60 - -VALUE_CALL = "dialing" -VALUE_CONNECT = "talking" -VALUE_DEFAULT = "idle" -VALUE_DISCONNECT = "idle" -VALUE_RING = "ringing" - -# Return cached results if phonebook was downloaded less then this time ago. -MIN_TIME_PHONEBOOK_UPDATE = datetime.timedelta(hours=6) -SCAN_INTERVAL = datetime.timedelta(hours=3) +SCAN_INTERVAL = timedelta(hours=3) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, vol.Optional(CONF_USERNAME, default=DEFAULT_USERNAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PHONEBOOK, default=DEFAULT_PHONEBOOK): cv.positive_int, - vol.Optional(CONF_PREFIXES, default=[]): vol.All(cv.ensure_list, [cv.string]), + vol.Optional(CONF_PREFIXES): vol.All(cv.ensure_list, [cv.string]), } ) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up Fritz!Box call monitor sensor platform.""" - name = config[CONF_NAME] - host = config[CONF_HOST] - # Try to resolve a hostname; if it is already an IP, it will be returned as-is - try: - host = socket.gethostbyname(host) - except OSError: - _LOGGER.error("Could not resolve hostname %s", host) - return - port = config[CONF_PORT] - username = config[CONF_USERNAME] - password = config.get(CONF_PASSWORD) - phonebook_id = config[CONF_PHONEBOOK] - prefixes = config[CONF_PREFIXES] - - try: - phonebook = FritzBoxPhonebook( - host=host, - port=port, - username=username, - password=password, - phonebook_id=phonebook_id, - prefixes=prefixes, +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Import the platform into a config entry.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config ) - except: # noqa: E722 pylint: disable=bare-except - phonebook = None - _LOGGER.warning("Phonebook with ID %s not found on Fritz!Box", phonebook_id) + ) - sensor = FritzBoxCallSensor(name=name, phonebook=phonebook) - add_entities([sensor]) +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the fritzbox_callmonitor sensor from config_entry.""" + fritzbox_phonebook = hass.data[DOMAIN][config_entry.entry_id][FRITZBOX_PHONEBOOK] - monitor = FritzBoxCallMonitor(host=host, port=port, sensor=sensor) - monitor.connect() + phonebook_name = config_entry.title + phonebook_id = config_entry.data[CONF_PHONEBOOK] + prefixes = config_entry.options.get(CONF_PREFIXES) + serial_number = config_entry.data[SERIAL_NUMBER] + host = config_entry.data[CONF_HOST] + port = config_entry.data[CONF_PORT] - def _stop_listener(_event): - monitor.stopped.set() + name = f"{fritzbox_phonebook.fph.modelname} Call Monitor {phonebook_name}" + unique_id = f"{serial_number}-{phonebook_id}" - hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, _stop_listener) + sensor = FritzBoxCallSensor( + name=name, + unique_id=unique_id, + fritzbox_phonebook=fritzbox_phonebook, + prefixes=prefixes, + host=host, + port=port, + ) - return monitor.sock is not None + hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, sensor.async_will_remove_from_hass() + ) + + async_add_entities([sensor]) class FritzBoxCallSensor(Entity): """Implementation of a Fritz!Box call monitor.""" - def __init__(self, name, phonebook): + def __init__(self, name, unique_id, fritzbox_phonebook, prefixes, host, port): """Initialize the sensor.""" - self._state = VALUE_DEFAULT + self._state = STATE_IDLE self._attributes = {} - self._name = name - self.phonebook = phonebook + self._name = name.title() + self._unique_id = unique_id + self._fritzbox_phonebook = fritzbox_phonebook + self._prefixes = prefixes + self._host = host + self._port = port + self._monitor = None + + async def async_added_to_hass(self): + """Connect to FRITZ!Box to monitor its call state.""" + _LOGGER.debug("Starting monitor for: %s", self.entity_id) + self._monitor = FritzBoxCallMonitor( + host=self._host, + port=self._port, + sensor=self, + ) + self._monitor.connect() + + async def async_will_remove_from_hass(self): + """Disconnect from FRITZ!Box by stopping monitor.""" + if ( + self._monitor + and self._monitor.stopped + and not self._monitor.stopped.is_set() + and self._monitor.connection + and self._monitor.connection.is_alive + ): + self._monitor.stopped.set() + self._monitor.connection.stop() + _LOGGER.debug("Stopped monitor for: %s", self.entity_id) def set_state(self, state): """Set the state.""" @@ -120,10 +148,15 @@ class FritzBoxCallSensor(Entity): """Set the state attributes.""" self._attributes = attributes + @property + def name(self): + """Return name of this sensor.""" + return self._name + @property def should_poll(self): """Only poll to update phonebook, if defined.""" - return self.phonebook is not None + return self._fritzbox_phonebook is not None @property def state(self): @@ -131,25 +164,43 @@ class FritzBoxCallSensor(Entity): return self._state @property - def name(self): - """Return the name of the sensor.""" - return self._name + def icon(self): + """Return the icon of the sensor.""" + return ICON_PHONE @property def device_state_attributes(self): """Return the state attributes.""" + if self._prefixes: + self._attributes[ATTR_PREFIXES] = self._prefixes return self._attributes + @property + def device_info(self): + """Return device specific attributes.""" + return { + "name": self._fritzbox_phonebook.fph.modelname, + "identifiers": {(DOMAIN, self._unique_id)}, + "manufacturer": MANUFACTURER, + "model": self._fritzbox_phonebook.fph.modelname, + "sw_version": self._fritzbox_phonebook.fph.fc.system_version, + } + + @property + def unique_id(self): + """Return the unique ID of the device.""" + return self._unique_id + def number_to_name(self, number): """Return a name for a given phone number.""" - if self.phonebook is None: - return "unknown" - return self.phonebook.get_name(number) + if self._fritzbox_phonebook is None: + return UNKOWN_NAME + return self._fritzbox_phonebook.get_name(number) def update(self): """Update the phonebook if it is defined.""" - if self.phonebook is not None: - self.phonebook.update_phonebook() + if self._fritzbox_phonebook is not None: + self._fritzbox_phonebook.update_phonebook() class FritzBoxCallMonitor: @@ -159,142 +210,78 @@ class FritzBoxCallMonitor: """Initialize Fritz!Box monitor instance.""" self.host = host self.port = port - self.sock = None + self.connection = None + self.stopped = ThreadingEvent() self._sensor = sensor - self.stopped = threading.Event() def connect(self): """Connect to the Fritz!Box.""" - _LOGGER.debug("Setting up socket...") - self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self.sock.settimeout(10) - self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + _LOGGER.debug("Setting up socket connection") try: - self.sock.connect((self.host, self.port)) - threading.Thread(target=self._listen).start() + self.connection = FritzMonitor(address=self.host, port=self.port) + kwargs = {"event_queue": self.connection.start()} + Thread(target=self._process_events, kwargs=kwargs).start() except OSError as err: - self.sock = None + self.connection = None _LOGGER.error( "Cannot connect to %s on port %s: %s", self.host, self.port, err ) - def _listen(self): + def _process_events(self, event_queue): """Listen to incoming or outgoing calls.""" - _LOGGER.debug("Connection established, waiting for response...") - while not self.stopped.isSet(): + _LOGGER.debug("Connection established, waiting for events") + while not self.stopped.is_set(): try: - response = self.sock.recv(2048) - except socket.timeout: - # if no response after 10 seconds, just recv again + event = event_queue.get(timeout=10) + except queue.Empty: + if not self.connection.is_alive and not self.stopped.is_set(): + _LOGGER.error("Connection has abruptly ended") + _LOGGER.debug("Empty event queue") continue - response = str(response, "utf-8") - _LOGGER.debug("Received %s", response) - - if not response: - # if the response is empty, the connection has been lost. - # try to reconnect - _LOGGER.warning("Connection lost, reconnecting...") - self.sock = None - while self.sock is None: - self.connect() - time.sleep(INTERVAL_RECONNECT) else: - line = response.split("\n", 1)[0] - self._parse(line) - time.sleep(1) + _LOGGER.debug("Received event: %s", event) + self._parse(event) + sleep(1) def _parse(self, line): """Parse the call information and set the sensor states.""" line = line.split(";") df_in = "%d.%m.%y %H:%M:%S" df_out = "%Y-%m-%dT%H:%M:%S" - isotime = datetime.datetime.strptime(line[0], df_in).strftime(df_out) - if line[1] == "RING": - self._sensor.set_state(VALUE_RING) + isotime = datetime.strptime(line[0], df_in).strftime(df_out) + if line[1] == FRITZ_STATE_RING: + self._sensor.set_state(STATE_RINGING) att = { "type": "incoming", "from": line[3], "to": line[4], "device": line[5], "initiated": isotime, + "from_name": self._sensor.number_to_name(line[3]), } - att["from_name"] = self._sensor.number_to_name(att["from"]) self._sensor.set_attributes(att) - elif line[1] == "CALL": - self._sensor.set_state(VALUE_CALL) + elif line[1] == FRITZ_STATE_CALL: + self._sensor.set_state(STATE_DIALING) att = { "type": "outgoing", "from": line[4], "to": line[5], "device": line[6], "initiated": isotime, + "to_name": self._sensor.number_to_name(line[5]), } - att["to_name"] = self._sensor.number_to_name(att["to"]) self._sensor.set_attributes(att) - elif line[1] == "CONNECT": - self._sensor.set_state(VALUE_CONNECT) - att = {"with": line[4], "device": line[3], "accepted": isotime} - att["with_name"] = self._sensor.number_to_name(att["with"]) + elif line[1] == FRITZ_STATE_CONNECT: + self._sensor.set_state(STATE_TALKING) + att = { + "with": line[4], + "device": line[3], + "accepted": isotime, + "with_name": self._sensor.number_to_name(line[4]), + } self._sensor.set_attributes(att) - elif line[1] == "DISCONNECT": - self._sensor.set_state(VALUE_DISCONNECT) + elif line[1] == FRITZ_STATE_DISCONNECT: + self._sensor.set_state(STATE_IDLE) att = {"duration": line[3], "closed": isotime} self._sensor.set_attributes(att) self._sensor.schedule_update_ha_state() - - -class FritzBoxPhonebook: - """This connects to a FritzBox router and downloads its phone book.""" - - def __init__(self, host, port, username, password, phonebook_id=0, prefixes=None): - """Initialize the class.""" - self.host = host - self.username = username - self.password = password - self.port = port - self.phonebook_id = phonebook_id - self.phonebook_dict = None - self.number_dict = None - self.prefixes = prefixes or [] - - # Establish a connection to the FRITZ!Box. - self.fph = FritzPhonebook( - address=self.host, user=self.username, password=self.password - ) - - if self.phonebook_id not in self.fph.list_phonebooks: - raise ValueError("Phonebook with this ID not found.") - - self.update_phonebook() - - @Throttle(MIN_TIME_PHONEBOOK_UPDATE) - def update_phonebook(self): - """Update the phone book dictionary.""" - self.phonebook_dict = self.fph.get_all_names(self.phonebook_id) - self.number_dict = { - re.sub(r"[^\d\+]", "", nr): name - for name, nrs in self.phonebook_dict.items() - for nr in nrs - } - _LOGGER.info("Fritz!Box phone book successfully updated") - - def get_name(self, number): - """Return a name for a given phone number.""" - number = re.sub(r"[^\d\+]", "", str(number)) - if self.number_dict is None: - return "unknown" - try: - return self.number_dict[number] - except KeyError: - pass - if self.prefixes: - for prefix in self.prefixes: - try: - return self.number_dict[prefix + number] - except KeyError: - pass - try: - return self.number_dict[prefix + number.lstrip("0")] - except KeyError: - pass - return "unknown" diff --git a/homeassistant/components/fritzbox_callmonitor/strings.json b/homeassistant/components/fritzbox_callmonitor/strings.json new file mode 100644 index 00000000000..d0325a07637 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/strings.json @@ -0,0 +1,41 @@ +{ + "config": { + "flow_title": "AVM FRITZ!Box call monitor: {name}", + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + }, + "phonebook": { + "data": { + "phonebook": "Phonebook" + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", + "insufficient_permissions": "User has insufficient permissions to access AVM FRITZ!Box settings and its phonebooks." + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]" + } + }, + "options": { + "step": { + "init": { + "title": "Configure Prefixes", + "data": { + "prefixes": "Prefixes (comma separated list)" + } + } + }, + "error": { + "malformed_prefixes": "Prefixes are malformed, please check their format." + } + } +} diff --git a/homeassistant/components/fritzbox_callmonitor/translations/ca.json b/homeassistant/components/fritzbox_callmonitor/translations/ca.json new file mode 100644 index 00000000000..808b642f4ff --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/ca.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "insufficient_permissions": "L'usuari no t\u00e9 permisos suficients per accedir a la configuraci\u00f3 d'AVM FRITZ!Box i les seves agendes.", + "no_devices_found": "No s'han trobat dispositius a la xarxa" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida" + }, + "flow_title": "Sensor de trucades d'AVM FRITZ!Box: {name}", + "step": { + "phonebook": { + "data": { + "phonebook": "Agenda" + } + }, + "user": { + "data": { + "host": "Amfitri\u00f3", + "password": "Contrasenya", + "port": "Port", + "username": "Nom d'usuari" + } + } + } + }, + "options": { + "error": { + "malformed_prefixes": "El format dels prefixos no \u00e9s correcte, comprova'l." + }, + "step": { + "init": { + "data": { + "prefixes": "Prefixos (llista separada per comes)" + }, + "title": "Configuraci\u00f3 dels prefixos" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/cs.json b/homeassistant/components/fritzbox_callmonitor/translations/cs.json new file mode 100644 index 00000000000..c40da2900bc --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "no_devices_found": "V s\u00edti nebyla nalezena \u017e\u00e1dn\u00e1 za\u0159\u00edzen\u00ed" + }, + "error": { + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "password": "Heslo", + "port": "Port", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/en.json b/homeassistant/components/fritzbox_callmonitor/translations/en.json new file mode 100644 index 00000000000..286bed1d5bc --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/en.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "insufficient_permissions": "User has insufficient permissions to access AVM FRITZ!Box settings and its phonebooks.", + "no_devices_found": "No devices found on the network" + }, + "error": { + "invalid_auth": "Invalid authentication" + }, + "flow_title": "AVM FRITZ!Box call monitor: {name}", + "step": { + "phonebook": { + "data": { + "phonebook": "Phonebook" + } + }, + "user": { + "data": { + "host": "Host", + "password": "Password", + "port": "Port", + "username": "Username" + } + } + } + }, + "options": { + "error": { + "malformed_prefixes": "Prefixes are malformed, please check their format." + }, + "step": { + "init": { + "data": { + "prefixes": "Prefixes (comma separated list)" + }, + "title": "Configure Prefixes" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/et.json b/homeassistant/components/fritzbox_callmonitor/translations/et.json new file mode 100644 index 00000000000..7770f31ae0e --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/et.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "insufficient_permissions": "Kasutajal ei ole piisavalt \u00f5igusi juurdep\u00e4\u00e4suks AVM FRITZ! Box'i seadetele jatelefoniraamatutele.", + "no_devices_found": "V\u00f5rgust ei leitud \u00fchtegi seadet" + }, + "error": { + "invalid_auth": "Vigane autentimine" + }, + "flow_title": "AVM FRITZ! K\u00f5nekontroll: {name}", + "step": { + "phonebook": { + "data": { + "phonebook": "Telefoniraamat" + } + }, + "user": { + "data": { + "host": "Host", + "password": "Salas\u00f5na", + "port": "Port", + "username": "Kasutajanimi" + } + } + } + }, + "options": { + "error": { + "malformed_prefixes": "Eesliited on valesti vormindatud, kontrolli nende vormingut." + }, + "step": { + "init": { + "data": { + "prefixes": "Eesliited (komadega eraldatud loend)" + }, + "title": "Eesliidete seadistamine" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/it.json b/homeassistant/components/fritzbox_callmonitor/translations/it.json new file mode 100644 index 00000000000..5696bf86fd1 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/it.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "insufficient_permissions": "L'utente non dispone di autorizzazioni sufficienti per accedere alle impostazioni di AVM FRITZ! Box e alle sue rubriche.", + "no_devices_found": "Nessun dispositivo trovato sulla rete" + }, + "error": { + "invalid_auth": "Autenticazione non valida" + }, + "flow_title": "Monitoraggio chiamate FRITZ! Box AVM: {name}", + "step": { + "phonebook": { + "data": { + "phonebook": "Rubrica telefonica" + } + }, + "user": { + "data": { + "host": "Host", + "password": "Password", + "port": "Porta", + "username": "Nome utente" + } + } + } + }, + "options": { + "error": { + "malformed_prefixes": "I prefissi non sono corretti, controlla il loro formato." + }, + "step": { + "init": { + "data": { + "prefixes": "Prefissi (elenco separato da virgole)" + }, + "title": "Configura prefissi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/lb.json b/homeassistant/components/fritzbox_callmonitor/translations/lb.json new file mode 100644 index 00000000000..67b5879a557 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/lb.json @@ -0,0 +1,33 @@ +{ + "config": { + "flow_title": "AVM FRITZ!Box Call Monitor: {name}", + "step": { + "phonebook": { + "data": { + "phonebook": "Adressbuch" + } + }, + "user": { + "data": { + "host": "Host", + "password": "Passwuert", + "port": "Port", + "username": "Benotzernumm" + } + } + } + }, + "options": { + "error": { + "malformed_prefixes": "Pr\u00e9fixe sinn am falsche Format, iwwerpr\u00e9if dat w.e.g" + }, + "step": { + "init": { + "data": { + "prefixes": "Pr\u00e9fixe (komma getrennte L\u00ebscht)" + }, + "title": "Pr\u00e9fixe konfigur\u00e9ieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/no.json b/homeassistant/components/fritzbox_callmonitor/translations/no.json new file mode 100644 index 00000000000..12883b0140d --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/no.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "insufficient_permissions": "Brukeren har utilstrekkelig tillatelse til \u00e5 f\u00e5 tilgang til AVM FRITZ! Box-innstillingene og telefonb\u00f8kene.", + "no_devices_found": "Ingen enheter funnet p\u00e5 nettverket" + }, + "error": { + "invalid_auth": "Ugyldig godkjenning" + }, + "flow_title": "AVM FRITZ! Box monitor: {name}", + "step": { + "phonebook": { + "data": { + "phonebook": "Telefonbok" + } + }, + "user": { + "data": { + "host": "Vert", + "password": "Passord", + "port": "Port", + "username": "Brukernavn" + } + } + } + }, + "options": { + "error": { + "malformed_prefixes": "Prefikser er misformet, vennligst sjekk deres format." + }, + "step": { + "init": { + "data": { + "prefixes": "Prefikser (kommaseparert liste)" + }, + "title": "Konfigurer prefiks" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/pl.json b/homeassistant/components/fritzbox_callmonitor/translations/pl.json new file mode 100644 index 00000000000..fa0317f5c9d --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/pl.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "insufficient_permissions": "U\u017cytkownik ma niewystarczaj\u0105ce uprawnienia, aby uzyska\u0107 dost\u0119p do ustawie\u0144 AVM FRITZ! Box i jego ksi\u0105\u017cek telefonicznych.", + "no_devices_found": "Nie znaleziono urz\u0105dze\u0144 w sieci" + }, + "error": { + "invalid_auth": "Niepoprawne uwierzytelnienie" + }, + "flow_title": "Monitor po\u0142\u0105cze\u0144 AVM FRITZ!Box: {name}", + "step": { + "phonebook": { + "data": { + "phonebook": "Ksi\u0105\u017cka telefoniczna" + } + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "password": "Has\u0142o", + "port": "Port", + "username": "Nazwa u\u017cytkownika" + } + } + } + }, + "options": { + "error": { + "malformed_prefixes": "Prefiksy s\u0105 nieprawid\u0142owe, prosz\u0119 sprawdzi\u0107 ich format." + }, + "step": { + "init": { + "data": { + "prefixes": "Prefiksy (lista oddzielona przecinkami)" + }, + "title": "Skonfiguruj prefiksy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/ru.json b/homeassistant/components/fritzbox_callmonitor/translations/ru.json new file mode 100644 index 00000000000..3eb432532c4 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/ru.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "insufficient_permissions": "\u0423 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u043d\u0435\u0434\u043e\u0441\u0442\u0430\u0442\u043e\u0447\u043d\u043e \u043f\u0440\u0430\u0432 \u0434\u043b\u044f \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c AVM FRITZ!Box \u0438 \u0435\u0433\u043e \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u043d\u044b\u043c \u043a\u043d\u0438\u0433\u0430\u043c.", + "no_devices_found": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b \u0432 \u0441\u0435\u0442\u0438." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f." + }, + "flow_title": "AVM FRITZ!Box call monitor: {name}", + "step": { + "phonebook": { + "data": { + "phonebook": "\u0422\u0435\u043b\u0435\u0444\u043e\u043d\u043d\u0430\u044f \u043a\u043d\u0438\u0433\u0430" + } + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041b\u043e\u0433\u0438\u043d" + } + } + } + }, + "options": { + "error": { + "malformed_prefixes": "\u041f\u0440\u0435\u0444\u0438\u043a\u0441\u044b \u0438\u043c\u0435\u044e\u0442 \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u0444\u043e\u0440\u043c\u0430\u0442, \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0438\u0445." + }, + "step": { + "init": { + "data": { + "prefixes": "\u041f\u0440\u0435\u0444\u0438\u043a\u0441\u044b (\u0441\u043f\u0438\u0441\u043e\u043a, \u0440\u0430\u0437\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0439 \u0437\u0430\u043f\u044f\u0442\u044b\u043c\u0438)" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u043e\u0432" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/tr.json b/homeassistant/components/fritzbox_callmonitor/translations/tr.json new file mode 100644 index 00000000000..76799f24af8 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/tr.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "insufficient_permissions": "Kullan\u0131c\u0131, AVM FRITZ! Box ayarlar\u0131na ve telefon defterlerine eri\u015fmek i\u00e7in yeterli izne sahip de\u011fil.", + "no_devices_found": "A\u011fda cihaz bulunamad\u0131" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "flow_title": "AVM FRITZ! Box \u00e7a\u011fr\u0131 monit\u00f6r\u00fc: {name}", + "step": { + "phonebook": { + "data": { + "phonebook": "Telefon rehberi" + } + }, + "user": { + "data": { + "host": "Ana Bilgisayar", + "password": "\u015eifre", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "options": { + "error": { + "malformed_prefixes": "\u00d6nekler yanl\u0131\u015f bi\u00e7imlendirilmi\u015ftir, l\u00fctfen bi\u00e7imlerini kontrol edin." + }, + "step": { + "init": { + "data": { + "prefixes": "\u00d6nekler (virg\u00fclle ayr\u0131lm\u0131\u015f liste)" + }, + "title": "\u00d6nekleri Yap\u0131land\u0131r" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox_callmonitor/translations/zh-Hant.json b/homeassistant/components/fritzbox_callmonitor/translations/zh-Hant.json new file mode 100644 index 00000000000..d159f5df0f9 --- /dev/null +++ b/homeassistant/components/fritzbox_callmonitor/translations/zh-Hant.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "insufficient_permissions": "\u4f7f\u7528\u8005\u6c92\u6709\u8db3\u5920\u6b0a\u9650\u4ee5\u5b58\u53d6 AVM FRITZ!Box \u8a2d\u5b9a\u53ca\u96fb\u8a71\u7c3f\u3002", + "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" + }, + "error": { + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548" + }, + "flow_title": "AVM FRITZ!Box \u901a\u8a71\u76e3\u63a7\u5668\uff1a{name}", + "step": { + "phonebook": { + "data": { + "phonebook": "\u96fb\u8a71\u7c3f" + } + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + }, + "options": { + "error": { + "malformed_prefixes": "\u524d\u7db4\u5b57\u9996\u683c\u5f0f\u932f\u8aa4\uff0c\u8acb\u518d\u78ba\u8a8d\u5176\u683c\u5f0f\u3002" + }, + "step": { + "init": { + "data": { + "prefixes": "\u524d\u7db4\u5b57\u9996\uff08\u4ee5\u9017\u865f\u5206\u9694\uff09" + }, + "title": "\u8a2d\u5b9a\u524d\u7db4\u5b57\u9996" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 241b07fd591..65a5497d1f9 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20201229.1"], + "requirements": ["home-assistant-frontend==20210127.7"], "dependencies": [ "api", "auth", diff --git a/homeassistant/components/garmin_connect/translations/de.json b/homeassistant/components/garmin_connect/translations/de.json index 54d27e9956e..9186f753a77 100644 --- a/homeassistant/components/garmin_connect/translations/de.json +++ b/homeassistant/components/garmin_connect/translations/de.json @@ -4,10 +4,10 @@ "already_configured": "Dieses Konto ist bereits konfiguriert." }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen. Bitte versuchen Sie es erneut.", - "invalid_auth": "Ung\u00fcltige Authentifizierung.", - "too_many_requests": "Zu viele Anfragen, wiederholen Sie es sp\u00e4ter.", - "unknown": "Unerwarteter Fehler." + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "too_many_requests": "Zu viele Anfragen, versuche es sp\u00e4ter erneut.", + "unknown": "Unerwarteter Fehler" }, "step": { "user": { diff --git a/homeassistant/components/garmin_connect/translations/tr.json b/homeassistant/components/garmin_connect/translations/tr.json new file mode 100644 index 00000000000..a83e1936fb4 --- /dev/null +++ b/homeassistant/components/garmin_connect/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/translations/uk.json b/homeassistant/components/garmin_connect/translations/uk.json new file mode 100644 index 00000000000..aef0632b0f1 --- /dev/null +++ b/homeassistant/components/garmin_connect/translations/uk.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "too_many_requests": "\u0417\u0430\u043d\u0430\u0434\u0442\u043e \u0431\u0430\u0433\u0430\u0442\u043e \u0437\u0430\u043f\u0438\u0442\u0456\u0432, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437 \u043f\u0456\u0437\u043d\u0456\u0448\u0435.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0412\u0430\u0448\u0456 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0456 \u0434\u0430\u043d\u0456.", + "title": "Garmin Connect" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/translations/de.json b/homeassistant/components/gdacs/translations/de.json index 07d1a4bdb79..a69295f0640 100644 --- a/homeassistant/components/gdacs/translations/de.json +++ b/homeassistant/components/gdacs/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Der Standort ist bereits konfiguriert." + "already_configured": "Der Dienst ist bereits konfiguriert" }, "step": { "user": { diff --git a/homeassistant/components/gdacs/translations/tr.json b/homeassistant/components/gdacs/translations/tr.json new file mode 100644 index 00000000000..aeb6a5a345e --- /dev/null +++ b/homeassistant/components/gdacs/translations/tr.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "user": { + "data": { + "radius": "Yar\u0131\u00e7ap" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gdacs/translations/uk.json b/homeassistant/components/gdacs/translations/uk.json new file mode 100644 index 00000000000..0ab20bc55a3 --- /dev/null +++ b/homeassistant/components/gdacs/translations/uk.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "step": { + "user": { + "data": { + "radius": "\u0420\u0430\u0434\u0456\u0443\u0441" + }, + "title": "\u041c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index 175ee8f1d5b..433e91104ad 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -276,6 +276,13 @@ class GenericThermostat(ClimateEntity, RestoreEntity): return self._temp_precision return super().precision + @property + def target_temperature_step(self): + """Return the supported step of target temperature.""" + # Since this integration does not yet have a step size parameter + # we have to re-use the precision as the step size for now. + return self.precision + @property def temperature_unit(self): """Return the unit of measurement.""" diff --git a/homeassistant/components/geofency/translations/de.json b/homeassistant/components/geofency/translations/de.json index 31b8a5eb321..9c3fd3ea1b0 100644 --- a/homeassistant/components/geofency/translations/de.json +++ b/homeassistant/components/geofency/translations/de.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", + "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." + }, "create_entry": { "default": "Um Ereignisse an den Home Assistant zu senden, musst das Webhook Feature in Geofency konfiguriert werden.\n\n F\u00fcge die folgenden Informationen ein: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n \n Weitere Informationen finden sich in der [Dokumentation]({docs_url})." }, diff --git a/homeassistant/components/geofency/translations/tr.json b/homeassistant/components/geofency/translations/tr.json new file mode 100644 index 00000000000..84adcdf8225 --- /dev/null +++ b/homeassistant/components/geofency/translations/tr.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/translations/uk.json b/homeassistant/components/geofency/translations/uk.json new file mode 100644 index 00000000000..54a14afb764 --- /dev/null +++ b/homeassistant/components/geofency/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "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.", + "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c." + }, + "create_entry": { + "default": "\u0414\u043b\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043a\u0438 \u043f\u043e\u0434\u0456\u0439 \u0432 Home Assistant \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Webhook \u0434\u043b\u044f Geofency. \n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e: \n\n- URL: `{webhook_url}`\n- Method: POST \n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457." + }, + "step": { + "user": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Geofency?", + "title": "Geofency" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/translations/tr.json b/homeassistant/components/geonetnz_quakes/translations/tr.json new file mode 100644 index 00000000000..717f6d72b94 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/translations/uk.json b/homeassistant/components/geonetnz_quakes/translations/uk.json new file mode 100644 index 00000000000..35653baa945 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/translations/uk.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "\u0420\u0430\u0434\u0456\u0443\u0441" + }, + "title": "GeoNet NZ Quakes" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/translations/de.json b/homeassistant/components/geonetnz_volcano/translations/de.json index b573d93cd5a..a29555e53ab 100644 --- a/homeassistant/components/geonetnz_volcano/translations/de.json +++ b/homeassistant/components/geonetnz_volcano/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Standort ist bereits konfiguriert" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/geonetnz_volcano/translations/tr.json b/homeassistant/components/geonetnz_volcano/translations/tr.json new file mode 100644 index 00000000000..980be333568 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/translations/tr.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "user": { + "data": { + "radius": "Yar\u0131\u00e7ap" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/translations/uk.json b/homeassistant/components/geonetnz_volcano/translations/uk.json new file mode 100644 index 00000000000..77a4f1eee68 --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/translations/uk.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435." + }, + "step": { + "user": { + "data": { + "radius": "\u0420\u0430\u0434\u0456\u0443\u0441" + }, + "title": "GeoNet NZ Volcano" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/translations/de.json b/homeassistant/components/gios/translations/de.json index 0a5cea1819d..7bbb01cf18d 100644 --- a/homeassistant/components/gios/translations/de.json +++ b/homeassistant/components/gios/translations/de.json @@ -4,14 +4,14 @@ "already_configured": "GIO\u015a integration f\u00fcr diese Messstation ist bereits konfiguriert. " }, "error": { - "cannot_connect": "Es kann keine Verbindung zum GIO\u015a-Server hergestellt werden.", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_sensors_data": "Ung\u00fcltige Sensordaten f\u00fcr diese Messstation.", "wrong_station_id": "ID der Messstation ist nicht korrekt." }, "step": { "user": { "data": { - "name": "Name der Integration", + "name": "Name", "station_id": "ID der Messstation" }, "description": "Einrichtung von GIO\u015a (Polnische Hauptinspektion f\u00fcr Umweltschutz) Integration der Luftqualit\u00e4t. Wenn du Hilfe bei der Konfiguration ben\u00f6tigst, schaue hier: https://www.home-assistant.io/integrations/gios", diff --git a/homeassistant/components/gios/translations/fr.json b/homeassistant/components/gios/translations/fr.json index b06c41208bc..2b02b5cfea0 100644 --- a/homeassistant/components/gios/translations/fr.json +++ b/homeassistant/components/gios/translations/fr.json @@ -18,5 +18,10 @@ "title": "GIO\u015a (Inspection g\u00e9n\u00e9rale polonaise de la protection de l'environnement)" } } + }, + "system_health": { + "info": { + "can_reach_server": "Acc\u00e9der au serveur GIO\u015a" + } } } \ No newline at end of file diff --git a/homeassistant/components/gios/translations/it.json b/homeassistant/components/gios/translations/it.json index 26bf8386d66..5d1e99d17f4 100644 --- a/homeassistant/components/gios/translations/it.json +++ b/homeassistant/components/gios/translations/it.json @@ -21,7 +21,7 @@ }, "system_health": { "info": { - "can_reach_server": "Raggiungi il server GIO\u015a" + "can_reach_server": "Server GIO\u015a raggiungibile" } } } \ No newline at end of file diff --git a/homeassistant/components/gios/translations/lb.json b/homeassistant/components/gios/translations/lb.json index 8e8ab861b43..cafea72fb78 100644 --- a/homeassistant/components/gios/translations/lb.json +++ b/homeassistant/components/gios/translations/lb.json @@ -18,5 +18,10 @@ "title": "GIO\u015a (Polnesch Chefinspektorat vum \u00cbmweltschutz)" } } + }, + "system_health": { + "info": { + "can_reach_server": "GIO\u015a Server ereechbar" + } } } \ No newline at end of file diff --git a/homeassistant/components/gios/translations/tr.json b/homeassistant/components/gios/translations/tr.json new file mode 100644 index 00000000000..590aec1894c --- /dev/null +++ b/homeassistant/components/gios/translations/tr.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/translations/uk.json b/homeassistant/components/gios/translations/uk.json new file mode 100644 index 00000000000..f62408c5e8e --- /dev/null +++ b/homeassistant/components/gios/translations/uk.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_sensors_data": "\u041d\u0435\u0432\u0456\u0440\u043d\u0456 \u0434\u0430\u043d\u0456 \u0441\u0435\u043d\u0441\u043e\u0440\u0456\u0432 \u0434\u043b\u044f \u0446\u0456\u0454\u0457 \u0432\u0438\u043c\u0456\u0440\u044e\u0432\u0430\u043b\u044c\u043d\u043e\u0457 \u0441\u0442\u0430\u043d\u0446\u0456\u0457.", + "wrong_station_id": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 ID \u0432\u0438\u043c\u0456\u0440\u044e\u0432\u0430\u043b\u044c\u043d\u043e\u0457 \u0441\u0442\u0430\u043d\u0446\u0456\u0457." + }, + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430", + "station_id": "ID \u0432\u0438\u043c\u0456\u0440\u044e\u0432\u0430\u043b\u044c\u043d\u043e\u0457 \u0441\u0442\u0430\u043d\u0446\u0456\u0457" + }, + "description": "\u0406\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044f \u043f\u0440\u043e \u044f\u043a\u0456\u0441\u0442\u044c \u043f\u043e\u0432\u0456\u0442\u0440\u044f \u0432\u0456\u0434 \u041f\u043e\u043b\u044c\u0441\u044c\u043a\u043e\u0457 \u0456\u043d\u0441\u043f\u0435\u043a\u0446\u0456\u0457 \u0437 \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \u043d\u0430\u0432\u043a\u043e\u043b\u0438\u0448\u043d\u044c\u043e\u0433\u043e \u0441\u0435\u0440\u0435\u0434\u043e\u0432\u0438\u0449\u0430 (GIO\u015a). \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0454\u044e \u043f\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044e \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457: https://www.home-assistant.io/integrations/gios.", + "title": "GIO\u015a (\u041f\u043e\u043b\u044c\u0441\u044c\u043a\u0430 \u0456\u043d\u0441\u043f\u0435\u043a\u0446\u0456\u044f \u0437 \u043e\u0445\u043e\u0440\u043e\u043d\u0438 \u043d\u0430\u0432\u043a\u043e\u043b\u0438\u0448\u043d\u044c\u043e\u0433\u043e \u0441\u0435\u0440\u0435\u0434\u043e\u0432\u0438\u0449\u0430)" + } + } + }, + "system_health": { + "info": { + "can_reach_server": "\u0414\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 GIO\u015a" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/translations/de.json b/homeassistant/components/glances/translations/de.json index 69c34907f19..e464bfdee34 100644 --- a/homeassistant/components/glances/translations/de.json +++ b/homeassistant/components/glances/translations/de.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Host ist bereits konfiguriert." + "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung zum Host nicht m\u00f6glich", + "cannot_connect": "Verbindung fehlgeschlagen", "wrong_version": "Version nicht unterst\u00fctzt (nur 2 oder 3)" }, "step": { diff --git a/homeassistant/components/glances/translations/tr.json b/homeassistant/components/glances/translations/tr.json new file mode 100644 index 00000000000..69f0cd7ceb1 --- /dev/null +++ b/homeassistant/components/glances/translations/tr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "G\u00fcncelleme s\u0131kl\u0131\u011f\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/glances/translations/uk.json b/homeassistant/components/glances/translations/uk.json new file mode 100644 index 00000000000..1fab197fe42 --- /dev/null +++ b/homeassistant/components/glances/translations/uk.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "wrong_version": "\u041f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u044e\u0442\u044c\u0441\u044f \u0442\u0456\u043b\u044c\u043a\u0438 \u0432\u0435\u0440\u0441\u0456\u0457 2 \u0442\u0430 3." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", + "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL", + "version": "\u0412\u0435\u0440\u0441\u0456\u044f API Glances (2 \u0430\u0431\u043e 3)" + }, + "title": "Glances" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f" + }, + "description": "\u0420\u043e\u0437\u0448\u0438\u0440\u0435\u043d\u0456 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 Glances" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/de.json b/homeassistant/components/goalzero/translations/de.json index d79c03f0179..7d8962cdb11 100644 --- a/homeassistant/components/goalzero/translations/de.json +++ b/homeassistant/components/goalzero/translations/de.json @@ -1,9 +1,20 @@ { "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_host": "Ung\u00fcltiger Hostname oder IP Adresse", "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Name" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/fr.json b/homeassistant/components/goalzero/translations/fr.json index 5c4b7a01580..7bd4929ad92 100644 --- a/homeassistant/components/goalzero/translations/fr.json +++ b/homeassistant/components/goalzero/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9" + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" }, "error": { "cannot_connect": "\u00c9chec de connexion", diff --git a/homeassistant/components/goalzero/translations/tr.json b/homeassistant/components/goalzero/translations/tr.json new file mode 100644 index 00000000000..ae77262b2b3 --- /dev/null +++ b/homeassistant/components/goalzero/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/goalzero/translations/uk.json b/homeassistant/components/goalzero/translations/uk.json new file mode 100644 index 00000000000..6d67d949c28 --- /dev/null +++ b/homeassistant/components/goalzero/translations/uk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_host": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "description": "\u0421\u043f\u043e\u0447\u0430\u0442\u043a\u0443 \u0412\u0430\u043c \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0437\u0430\u0432\u0430\u043d\u0442\u0430\u0436\u0438\u0442\u0438 \u0434\u043e\u0434\u0430\u0442\u043e\u043a Goal Zero: https://www.goalzero.com/product-features/yeti-app/. \n\n \u0414\u043e\u0442\u0440\u0438\u043c\u0443\u0439\u0442\u0435\u0441\u044c \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0439 \u043f\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044e Yeti \u0434\u043e \u043c\u0435\u0440\u0435\u0436\u0456 WiFi. \u041f\u043e\u0442\u0456\u043c \u0434\u0456\u0437\u043d\u0430\u0439\u0442\u0435\u0441\u044f IP \u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e, \u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434, \u0437 \u0412\u0430\u0448\u043e\u0433\u043e \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430. \u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0431\u0443\u0442\u0438 \u0442\u0430\u043a\u0438\u043c\u0438, \u0449\u043e\u0431 IP \u0430\u0434\u0440\u0435\u0441\u0430 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u043d\u0435 \u0437\u043c\u0456\u043d\u044e\u0432\u0430\u043b\u0430\u0441\u044c \u0437 \u0447\u0430\u0441\u043e\u043c. \u041f\u0440\u043e \u0442\u0435, \u044f\u043a \u0446\u0435 \u0437\u0440\u043e\u0431\u0438\u0442\u0438, \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0456\u0437\u043d\u0430\u0442\u0438\u0441\u044f \u0432 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0457 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u0412\u0430\u0448\u043e\u0433\u043e \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430.", + "title": "Goal Zero Yeti" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/common.py b/homeassistant/components/gogogate2/common.py index d17dd7033fe..2817c351013 100644 --- a/homeassistant/components/gogogate2/common.py +++ b/homeassistant/components/gogogate2/common.py @@ -70,7 +70,7 @@ def get_data_update_coordinator( async def async_update_data(): try: - return await hass.async_add_executor_job(api.info) + return await api.async_info() except Exception as exception: raise UpdateFailed( f"Error communicating with API: {exception}" diff --git a/homeassistant/components/gogogate2/config_flow.py b/homeassistant/components/gogogate2/config_flow.py index 5779ccaaa65..0c3f1b3653c 100644 --- a/homeassistant/components/gogogate2/config_flow.py +++ b/homeassistant/components/gogogate2/config_flow.py @@ -60,9 +60,7 @@ class Gogogate2FlowHandler(ConfigFlow, domain=DOMAIN): if user_input: api = get_api(user_input) try: - data: AbstractInfoResponse = await self.hass.async_add_executor_job( - api.info - ) + data: AbstractInfoResponse = await api.async_info() data_dict = dataclasses.asdict(data) title = data_dict.get( "gogogatename", data_dict.get("ismartgatename", "Cover") diff --git a/homeassistant/components/gogogate2/cover.py b/homeassistant/components/gogogate2/cover.py index 08520ad46ee..8b83073d0c8 100644 --- a/homeassistant/components/gogogate2/cover.py +++ b/homeassistant/components/gogogate2/cover.py @@ -129,15 +129,11 @@ class DeviceCover(CoordinatorEntity, CoverEntity): async def async_open_cover(self, **kwargs): """Open the door.""" - await self.hass.async_add_executor_job( - self._api.open_door, self._get_door().door_id - ) + await self._api.async_open_door(self._get_door().door_id) async def async_close_cover(self, **kwargs): """Close the door.""" - await self.hass.async_add_executor_job( - self._api.close_door, self._get_door().door_id - ) + await self._api.async_close_door(self._get_door().door_id) @property def state_attributes(self): diff --git a/homeassistant/components/gogogate2/manifest.json b/homeassistant/components/gogogate2/manifest.json index 893294da25e..b21eeace466 100644 --- a/homeassistant/components/gogogate2/manifest.json +++ b/homeassistant/components/gogogate2/manifest.json @@ -3,7 +3,7 @@ "name": "Gogogate2 and iSmartGate", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/gogogate2", - "requirements": ["gogogate2-api==2.0.3"], + "requirements": ["gogogate2-api==3.0.0"], "codeowners": ["@vangorra"], "homekit": { "models": [ diff --git a/homeassistant/components/gogogate2/translations/tr.json b/homeassistant/components/gogogate2/translations/tr.json new file mode 100644 index 00000000000..e912e7f8012 --- /dev/null +++ b/homeassistant/components/gogogate2/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "ip_address": "\u0130p Adresi", + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gogogate2/translations/uk.json b/homeassistant/components/gogogate2/translations/uk.json new file mode 100644 index 00000000000..c88b9b60384 --- /dev/null +++ b/homeassistant/components/gogogate2/translations/uk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "step": { + "user": { + "data": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 GogoGate2.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f GogoGate2 \u0430\u0431\u043e iSmartGate" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/google/manifest.json b/homeassistant/components/google/manifest.json index 1c14609f508..6df116effa5 100644 --- a/homeassistant/components/google/manifest.json +++ b/homeassistant/components/google/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/google", "requirements": [ "google-api-python-client==1.6.4", - "httplib2==0.10.3", + "httplib2==0.18.1", "oauth2client==4.0.0" ], "codeowners": [] diff --git a/homeassistant/components/google_assistant/helpers.py b/homeassistant/components/google_assistant/helpers.py index 00633422939..b4900d83b64 100644 --- a/homeassistant/components/google_assistant/helpers.py +++ b/homeassistant/components/google_assistant/helpers.py @@ -4,7 +4,7 @@ from asyncio import gather from collections.abc import Mapping import logging import pprint -from typing import List, Optional +from typing import Dict, List, Optional, Tuple from aiohttp.web import json_response @@ -18,6 +18,8 @@ from homeassistant.const import ( ) from homeassistant.core import Context, HomeAssistant, State, callback from homeassistant.helpers.area_registry import AreaEntry +from homeassistant.helpers.device_registry import DeviceEntry +from homeassistant.helpers.entity_registry import RegistryEntry from homeassistant.helpers.event import async_call_later from homeassistant.helpers.network import get_url from homeassistant.helpers.storage import Store @@ -40,29 +42,50 @@ SYNC_DELAY = 15 _LOGGER = logging.getLogger(__name__) -async def _get_area(hass, entity_id) -> Optional[AreaEntry]: - """Calculate the area for a entity_id.""" - dev_reg, ent_reg, area_reg = await gather( +async def _get_entity_and_device( + hass, entity_id +) -> Optional[Tuple[RegistryEntry, DeviceEntry]]: + """Fetch the entity and device entries for a entity_id.""" + dev_reg, ent_reg = await gather( hass.helpers.device_registry.async_get_registry(), hass.helpers.entity_registry.async_get_registry(), - hass.helpers.area_registry.async_get_registry(), ) entity_entry = ent_reg.async_get(entity_id) if not entity_entry: + return None, None + device_entry = dev_reg.devices.get(entity_entry.device_id) + return entity_entry, device_entry + + +async def _get_area(hass, entity_entry, device_entry) -> Optional[AreaEntry]: + """Calculate the area for an entity.""" + if entity_entry and entity_entry.area_id: + area_id = entity_entry.area_id + elif device_entry and device_entry.area_id: + area_id = device_entry.area_id + else: return None - if entity_entry.area_id: - area_id = entity_entry.area_id - else: - device_entry = dev_reg.devices.get(entity_entry.device_id) - if not (device_entry and device_entry.area_id): - return None - area_id = device_entry.area_id - + area_reg = await hass.helpers.area_registry.async_get_registry() return area_reg.areas.get(area_id) +async def _get_device_info(device_entry) -> Optional[Dict[str, str]]: + """Retrieve the device info for a device.""" + if not device_entry: + return None + + device_info = {} + if device_entry.manufacturer: + device_info["manufacturer"] = device_entry.manufacturer + if device_entry.model: + device_info["model"] = device_entry.model + if device_entry.sw_version: + device_info["swVersion"] = device_entry.sw_version + return device_info + + class AbstractConfig(ABC): """Hold the configuration for Google Assistant.""" @@ -438,6 +461,9 @@ class GoogleEntity: name = (entity_config.get(CONF_NAME) or state.name).strip() domain = state.domain device_class = state.attributes.get(ATTR_DEVICE_CLASS) + entity_entry, device_entry = await _get_entity_and_device( + self.hass, state.entity_id + ) traits = self.traits() @@ -475,10 +501,14 @@ class GoogleEntity: if room: device["roomHint"] = room else: - area = await _get_area(self.hass, state.entity_id) + area = await _get_area(self.hass, entity_entry, device_entry) if area and area.name: device["roomHint"] = area.name + device_info = await _get_device_info(device_entry) + if device_info: + device["deviceInfo"] = device_info + return device @callback diff --git a/homeassistant/components/google_assistant/report_state.py b/homeassistant/components/google_assistant/report_state.py index 8ffdad14140..8943d4d211e 100644 --- a/homeassistant/components/google_assistant/report_state.py +++ b/homeassistant/components/google_assistant/report_state.py @@ -4,7 +4,9 @@ import logging from homeassistant.const import MATCH_ALL from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.event import async_call_later +from homeassistant.helpers.significant_change import create_checker +from .const import DOMAIN from .error import SmartHomeError from .helpers import AbstractConfig, GoogleEntity, async_get_entities @@ -19,6 +21,7 @@ _LOGGER = logging.getLogger(__name__) @callback def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig): """Enable state reporting.""" + checker = None async def async_entity_state_listener(changed_entity, old_state, new_state): if not hass.is_running: @@ -35,25 +38,15 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig if not entity.is_supported(): return + if not checker.async_is_significant_change(new_state): + return + try: entity_data = entity.query_serialize() except SmartHomeError as err: _LOGGER.debug("Not reporting state for %s: %s", changed_entity, err.code) return - if old_state: - old_entity = GoogleEntity(hass, google_config, old_state) - - # Only report to Google if data that Google cares about has changed - try: - if entity_data == old_entity.query_serialize(): - return - except SmartHomeError: - # Happens if old state could not be serialized. - # In that case the data is different and should be - # reported. - pass - _LOGGER.debug("Reporting state for %s: %s", changed_entity, entity_data) await google_config.async_report_state_all( @@ -62,12 +55,20 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig async def inital_report(_now): """Report initially all states.""" + nonlocal unsub, checker entities = {} + checker = await create_checker(hass, DOMAIN) + for entity in async_get_entities(hass, google_config): if not entity.should_expose(): continue + # Tell our significant change checker that we're reporting + # So it knows with subsequent changes what was already reported. + if not checker.async_is_significant_change(entity.state): + continue + try: entities[entity.entity_id] = entity.query_serialize() except SmartHomeError: @@ -78,8 +79,11 @@ def async_enable_report_state(hass: HomeAssistant, google_config: AbstractConfig await google_config.async_report_state_all({"devices": {"states": entities}}) - async_call_later(hass, INITIAL_REPORT_DELAY, inital_report) + unsub = hass.helpers.event.async_track_state_change( + MATCH_ALL, async_entity_state_listener + ) - return hass.helpers.event.async_track_state_change( - MATCH_ALL, async_entity_state_listener - ) + unsub = async_call_later(hass, INITIAL_REPORT_DELAY, inital_report) + + # pylint: disable=unnecessary-lambda + return lambda: unsub() diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 8790c3c7402..b5dc2afd3e2 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -648,7 +648,14 @@ class StartStopTrait(_Trait): """Execute a StartStop command.""" if command == COMMAND_STARTSTOP: if params["start"] is False: - if self.state.state in (cover.STATE_CLOSING, cover.STATE_OPENING): + if ( + self.state.state + in ( + cover.STATE_CLOSING, + cover.STATE_OPENING, + ) + or self.state.attributes.get(ATTR_ASSUMED_STATE) + ): await self.hass.services.async_call( self.state.domain, cover.SERVICE_STOP_COVER, diff --git a/homeassistant/components/google_cloud/tts.py b/homeassistant/components/google_cloud/tts.py index 69b276fc75f..6ffa3a9acd1 100644 --- a/homeassistant/components/google_cloud/tts.py +++ b/homeassistant/components/google_cloud/tts.py @@ -25,6 +25,7 @@ CONF_TEXT_TYPE = "text_type" SUPPORTED_LANGUAGES = [ "ar-XA", "bn-IN", + "yue-HK", "cmn-CN", "cmn-TW", "cs-CZ", diff --git a/homeassistant/components/google_maps/manifest.json b/homeassistant/components/google_maps/manifest.json index 62791c212f9..435e01fb026 100644 --- a/homeassistant/components/google_maps/manifest.json +++ b/homeassistant/components/google_maps/manifest.json @@ -2,6 +2,6 @@ "domain": "google_maps", "name": "Google Maps", "documentation": "https://www.home-assistant.io/integrations/google_maps", - "requirements": ["locationsharinglib==4.1.0"], + "requirements": ["locationsharinglib==4.1.5"], "codeowners": [] } diff --git a/homeassistant/components/gpslogger/translations/de.json b/homeassistant/components/gpslogger/translations/de.json index d976a5fd663..7215f0c458f 100644 --- a/homeassistant/components/gpslogger/translations/de.json +++ b/homeassistant/components/gpslogger/translations/de.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", + "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." + }, "create_entry": { "default": "Um Ereignisse an Home Assistant zu senden, muss das Webhook Feature in der GPSLogger konfiguriert werden.\n\n F\u00fcge die folgenden Informationen ein: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n \n Weitere Informationen finden sich in der [Dokumentation]({docs_url})." }, diff --git a/homeassistant/components/gpslogger/translations/tr.json b/homeassistant/components/gpslogger/translations/tr.json new file mode 100644 index 00000000000..84adcdf8225 --- /dev/null +++ b/homeassistant/components/gpslogger/translations/tr.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gpslogger/translations/uk.json b/homeassistant/components/gpslogger/translations/uk.json new file mode 100644 index 00000000000..5b0b6305cdb --- /dev/null +++ b/homeassistant/components/gpslogger/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "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.", + "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c." + }, + "create_entry": { + "default": "\u0414\u043b\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043a\u0438 \u043f\u043e\u0434\u0456\u0439 \u0432 Home Assistant \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Webhook \u0434\u043b\u044f GPSLogger. \n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e: \n\n- URL: `{webhook_url}`\n- Method: POST \n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457." + }, + "step": { + "user": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 GPSLogger?", + "title": "GPSLogger" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gree/climate.py b/homeassistant/components/gree/climate.py index 6a33e3341b0..8d0170fbe50 100644 --- a/homeassistant/components/gree/climate.py +++ b/homeassistant/components/gree/climate.py @@ -193,7 +193,7 @@ class GreeClimateEntity(CoordinatorEntity, ClimateEntity): return HVAC_MODES.get(self.coordinator.device.mode) - async def async_set_hvac_mode(self, hvac_mode): + async def async_set_hvac_mode(self, hvac_mode) -> None: """Set new target hvac mode.""" if hvac_mode not in self.hvac_modes: raise ValueError(f"Invalid hvac_mode: {hvac_mode}") @@ -217,6 +217,22 @@ class GreeClimateEntity(CoordinatorEntity, ClimateEntity): await self.coordinator.push_state_update() self.async_write_ha_state() + async def async_turn_on(self) -> None: + """Turn on the device.""" + _LOGGER.debug("Turning on HVAC for device %s", self._name) + + self.coordinator.device.power = True + await self.coordinator.push_state_update() + self.async_write_ha_state() + + async def async_turn_off(self) -> None: + """Turn off the device.""" + _LOGGER.debug("Turning off HVAC for device %s", self._name) + + self.coordinator.device.power = False + await self.coordinator.push_state_update() + self.async_write_ha_state() + @property def hvac_modes(self) -> List[str]: """Return the HVAC modes support by the device.""" diff --git a/homeassistant/components/gree/translations/de.json b/homeassistant/components/gree/translations/de.json new file mode 100644 index 00000000000..96ed09a974f --- /dev/null +++ b/homeassistant/components/gree/translations/de.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "step": { + "confirm": { + "description": "M\u00f6chtest du mit der Einrichtung beginnen?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gree/translations/tr.json b/homeassistant/components/gree/translations/tr.json new file mode 100644 index 00000000000..8de4663957e --- /dev/null +++ b/homeassistant/components/gree/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "confirm": { + "description": "Kuruluma ba\u015flamak ister misiniz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gree/translations/uk.json b/homeassistant/components/gree/translations/uk.json new file mode 100644 index 00000000000..292861e9129 --- /dev/null +++ b/homeassistant/components/gree/translations/uk.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "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." + }, + "step": { + "confirm": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/griddy/translations/de.json b/homeassistant/components/griddy/translations/de.json index ad6a6e10ab0..4a6c477059c 100644 --- a/homeassistant/components/griddy/translations/de.json +++ b/homeassistant/components/griddy/translations/de.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Diese Ladezone ist bereits konfiguriert" + "already_configured": "Standort ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/griddy/translations/tr.json b/homeassistant/components/griddy/translations/tr.json index d887b148658..26e0fa73065 100644 --- a/homeassistant/components/griddy/translations/tr.json +++ b/homeassistant/components/griddy/translations/tr.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, "error": { "cannot_connect": "Ba\u011flant\u0131 kurulamad\u0131, l\u00fctfen tekrar deneyin", "unknown": "Beklenmeyen hata" diff --git a/homeassistant/components/griddy/translations/uk.json b/homeassistant/components/griddy/translations/uk.json new file mode 100644 index 00000000000..e366f0e8b24 --- /dev/null +++ b/homeassistant/components/griddy/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "loadzone": "\u0417\u043e\u043d\u0430 \u043d\u0430\u0432\u0430\u043d\u0442\u0430\u0436\u0435\u043d\u043d\u044f (\u0440\u043e\u0437\u0440\u0430\u0445\u0443\u043d\u043a\u043e\u0432\u0430 \u0442\u043e\u0447\u043a\u0430)" + }, + "description": "\u0417\u043e\u043d\u0430 \u043d\u0430\u0432\u0430\u043d\u0442\u0430\u0436\u0435\u043d\u043d\u044f \u0437\u043d\u0430\u0445\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0443 \u0432\u0430\u0448\u043e\u043c\u0443 \u043f\u0440\u043e\u0444\u0456\u043b\u0456 Griddy \u0432 \u0440\u043e\u0437\u0434\u0456\u043b\u0456 Account > Meter > Load Zone.", + "title": "Griddy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/group/translations/uk.json b/homeassistant/components/group/translations/uk.json index 2d57686134a..08cee558f27 100644 --- a/homeassistant/components/group/translations/uk.json +++ b/homeassistant/components/group/translations/uk.json @@ -9,7 +9,7 @@ "ok": "\u041e\u041a", "on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e", "open": "\u0412\u0456\u0434\u0447\u0438\u043d\u0435\u043d\u043e", - "problem": "\u0425\u0430\u043b\u0435\u043f\u0430", + "problem": "\u041f\u0440\u043e\u0431\u043b\u0435\u043c\u0430", "unlocked": "\u0420\u043e\u0437\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e" } }, diff --git a/homeassistant/components/guardian/translations/de.json b/homeassistant/components/guardian/translations/de.json index 27770d690f0..d1218cb2372 100644 --- a/homeassistant/components/guardian/translations/de.json +++ b/homeassistant/components/guardian/translations/de.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "cannot_connect": "Verbindungsfehler" + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "user": { diff --git a/homeassistant/components/guardian/translations/tr.json b/homeassistant/components/guardian/translations/tr.json new file mode 100644 index 00000000000..1e520a16995 --- /dev/null +++ b/homeassistant/components/guardian/translations/tr.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "ip_address": "\u0130p Adresi", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/guardian/translations/uk.json b/homeassistant/components/guardian/translations/uk.json new file mode 100644 index 00000000000..439a225895e --- /dev/null +++ b/homeassistant/components/guardian/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 Elexa Guardian." + }, + "zeroconf_confirm": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u0446\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 Elexa Guardian?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/de.json b/homeassistant/components/hangouts/translations/de.json index 5c8ab51cf4e..7b888cf531e 100644 --- a/homeassistant/components/hangouts/translations/de.json +++ b/homeassistant/components/hangouts/translations/de.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Google Hangouts ist bereits konfiguriert", - "unknown": "Ein unbekannter Fehler ist aufgetreten." + "unknown": "Unerwarteter Fehler" }, "error": { "invalid_2fa": "Ung\u00fcltige 2-Faktor Authentifizierung, bitte versuche es erneut.", diff --git a/homeassistant/components/hangouts/translations/tr.json b/homeassistant/components/hangouts/translations/tr.json new file mode 100644 index 00000000000..a204200a2d8 --- /dev/null +++ b/homeassistant/components/hangouts/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "email": "E-posta", + "password": "Parola" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/uk.json b/homeassistant/components/hangouts/translations/uk.json new file mode 100644 index 00000000000..93eb699d37c --- /dev/null +++ b/homeassistant/components/hangouts/translations/uk.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "invalid_2fa": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u044f, \u0431\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443.", + "invalid_2fa_method": "\u041d\u0435\u043f\u0440\u0438\u043f\u0443\u0441\u0442\u0438\u043c\u0438\u0439 \u0441\u043f\u043e\u0441\u0456\u0431 \u0434\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0457 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457 (\u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0438\u0442\u0438 \u043d\u0430 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0456).", + "invalid_login": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043b\u043e\u0433\u0456\u043d, \u0431\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443." + }, + "step": { + "2fa": { + "data": { + "2fa": "\u041f\u0456\u043d-\u043a\u043e\u0434 \u0434\u043b\u044f \u0434\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0457 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + }, + "description": "\u043f\u043e\u0440\u043e\u0436\u043d\u044c\u043e", + "title": "\u0414\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f" + }, + "user": { + "data": { + "authorization_code": "\u041a\u043e\u0434 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457 (\u0432\u0438\u043c\u0430\u0433\u0430\u0454\u0442\u044c\u0441\u044f \u0434\u043b\u044f \u0440\u0443\u0447\u043d\u043e\u0457 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457)", + "email": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u043f\u043e\u0440\u043e\u0436\u043d\u044c\u043e", + "title": "Google Hangouts" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py index e425b5ce94a..6ba63ee0f81 100644 --- a/homeassistant/components/harmony/__init__.py +++ b/homeassistant/components/harmony/__init__.py @@ -1,11 +1,7 @@ """The Logitech Harmony Hub integration.""" import asyncio -from homeassistant.components.remote import ( - ATTR_ACTIVITY, - ATTR_DELAY_SECS, - DEFAULT_DELAY_SECS, -) +from homeassistant.components.remote import ATTR_ACTIVITY, ATTR_DELAY_SECS from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant, callback @@ -13,7 +9,7 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import DOMAIN, HARMONY_OPTIONS_UPDATE, PLATFORMS -from .remote import HarmonyRemote +from .data import HarmonyData async def async_setup(hass: HomeAssistant, config: dict): @@ -33,22 +29,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): address = entry.data[CONF_HOST] name = entry.data[CONF_NAME] - activity = entry.options.get(ATTR_ACTIVITY) - delay_secs = entry.options.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS) - - harmony_conf_file = hass.config.path(f"harmony_{entry.unique_id}.conf") + data = HarmonyData(hass, address, name, entry.unique_id) try: - device = HarmonyRemote( - name, entry.unique_id, address, activity, harmony_conf_file, delay_secs - ) - connected_ok = await device.connect() + connected_ok = await data.connect() except (asyncio.TimeoutError, ValueError, AttributeError) as err: raise ConfigEntryNotReady from err if not connected_ok: raise ConfigEntryNotReady - hass.data[DOMAIN][entry.entry_id] = device + hass.data[DOMAIN][entry.entry_id] = data entry.add_update_listener(_update_listener) @@ -92,8 +82,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ) # Shutdown a harmony remote for removal - device = hass.data[DOMAIN][entry.entry_id] - await device.shutdown() + data = hass.data[DOMAIN][entry.entry_id] + await data.shutdown() if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py index 6d5adabe235..e01febbef43 100644 --- a/homeassistant/components/harmony/config_flow.py +++ b/homeassistant/components/harmony/config_flow.py @@ -129,19 +129,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): }, ) - async def async_step_import(self, validated_input): - """Handle import.""" - await self.async_set_unique_id( - validated_input[UNIQUE_ID], raise_on_progress=False - ) - self._abort_if_unique_id_configured() - - # Everything was validated in remote async_setup_platform - # all we do now is create. - return await self._async_create_entry_from_valid_input( - validated_input, validated_input - ) - @staticmethod @callback def async_get_options_flow(config_entry): diff --git a/homeassistant/components/harmony/connection_state.py b/homeassistant/components/harmony/connection_state.py new file mode 100644 index 00000000000..9706ba28776 --- /dev/null +++ b/homeassistant/components/harmony/connection_state.py @@ -0,0 +1,44 @@ +"""Mixin class for handling connection state changes.""" +import logging + +from homeassistant.helpers.event import async_call_later + +_LOGGER = logging.getLogger(__name__) + +TIME_MARK_DISCONNECTED = 10 + + +class ConnectionStateMixin: + """Base implementation for connection state handling.""" + + def __init__(self): + """Initialize this mixin instance.""" + super().__init__() + self._unsub_mark_disconnected = None + + async def got_connected(self, _=None): + """Notification that we're connected to the HUB.""" + _LOGGER.debug("%s: connected to the HUB", self._name) + self.async_write_ha_state() + + self._clear_disconnection_delay() + + async def got_disconnected(self, _=None): + """Notification that we're disconnected from the HUB.""" + _LOGGER.debug("%s: disconnected from the HUB", self._name) + # We're going to wait for 10 seconds before announcing we're + # unavailable, this to allow a reconnection to happen. + self._unsub_mark_disconnected = async_call_later( + self.hass, TIME_MARK_DISCONNECTED, self._mark_disconnected_if_unavailable + ) + + def _clear_disconnection_delay(self): + if self._unsub_mark_disconnected: + self._unsub_mark_disconnected() + self._unsub_mark_disconnected = None + + def _mark_disconnected_if_unavailable(self, _): + self._unsub_mark_disconnected = None + if not self.available: + # Still disconnected. Let the state engine know. + self.async_write_ha_state() diff --git a/homeassistant/components/harmony/const.py b/homeassistant/components/harmony/const.py index f6315b57b57..ee4a454847e 100644 --- a/homeassistant/components/harmony/const.py +++ b/homeassistant/components/harmony/const.py @@ -2,7 +2,7 @@ DOMAIN = "harmony" SERVICE_SYNC = "sync" SERVICE_CHANGE_CHANNEL = "change_channel" -PLATFORMS = ["remote"] +PLATFORMS = ["remote", "switch"] UNIQUE_ID = "unique_id" ACTIVITY_POWER_OFF = "PowerOff" HARMONY_OPTIONS_UPDATE = "harmony_options_update" diff --git a/homeassistant/components/harmony/data.py b/homeassistant/components/harmony/data.py new file mode 100644 index 00000000000..6c3ad874fa9 --- /dev/null +++ b/homeassistant/components/harmony/data.py @@ -0,0 +1,251 @@ +"""Harmony data object which contains the Harmony Client.""" + +import logging +from typing import Iterable + +from aioharmony.const import ClientCallbackType, SendCommandDevice +import aioharmony.exceptions as aioexc +from aioharmony.harmonyapi import HarmonyAPI as HarmonyClient + +from .const import ACTIVITY_POWER_OFF +from .subscriber import HarmonySubscriberMixin + +_LOGGER = logging.getLogger(__name__) + + +class HarmonyData(HarmonySubscriberMixin): + """HarmonyData registers for Harmony hub updates.""" + + def __init__(self, hass, address: str, name: str, unique_id: str): + """Initialize a data object.""" + super().__init__(hass) + self._name = name + self._unique_id = unique_id + self._available = False + + callbacks = { + "config_updated": self._config_updated, + "connect": self._connected, + "disconnect": self._disconnected, + "new_activity_starting": self._activity_starting, + "new_activity": self._activity_started, + } + self._client = HarmonyClient( + ip_address=address, callbacks=ClientCallbackType(**callbacks) + ) + + @property + def activity_names(self): + """Names of all the remotes activities.""" + activity_infos = self._client.config.get("activity", []) + activities = [activity["label"] for activity in activity_infos] + + # Remove both ways of representing PowerOff + if None in activities: + activities.remove(None) + if ACTIVITY_POWER_OFF in activities: + activities.remove(ACTIVITY_POWER_OFF) + + return activities + + @property + def device_names(self): + """Names of all of the devices connected to the hub.""" + device_infos = self._client.config.get("device", []) + devices = [device["label"] for device in device_infos] + + return devices + + @property + def name(self): + """Return the Harmony device's name.""" + return self._name + + @property + def unique_id(self): + """Return the Harmony device's unique_id.""" + return self._unique_id + + @property + def json_config(self): + """Return the hub config as json.""" + if self._client.config is None: + return None + return self._client.json_config + + @property + def available(self) -> bool: + """Return if connected to the hub.""" + return self._available + + @property + def current_activity(self) -> tuple: + """Return the current activity tuple.""" + return self._client.current_activity + + def device_info(self, domain: str): + """Return hub device info.""" + model = "Harmony Hub" + if "ethernetStatus" in self._client.hub_config.info: + model = "Harmony Hub Pro 2400" + return { + "identifiers": {(domain, self.unique_id)}, + "manufacturer": "Logitech", + "sw_version": self._client.hub_config.info.get( + "hubSwVersion", self._client.fw_version + ), + "name": self.name, + "model": model, + } + + async def connect(self) -> bool: + """Connect to the Harmony Hub.""" + _LOGGER.debug("%s: Connecting", self._name) + try: + if not await self._client.connect(): + _LOGGER.warning("%s: Unable to connect to HUB", self._name) + await self._client.close() + return False + except aioexc.TimeOut: + _LOGGER.warning("%s: Connection timed-out", self._name) + return False + return True + + async def shutdown(self): + """Close connection on shutdown.""" + _LOGGER.debug("%s: Closing Harmony Hub", self._name) + try: + await self._client.close() + except aioexc.TimeOut: + _LOGGER.warning("%s: Disconnect timed-out", self._name) + + async def async_start_activity(self, activity: str): + """Start an activity from the Harmony device.""" + + if not activity: + _LOGGER.error("%s: No activity specified with turn_on service", self.name) + return + + activity_id = None + activity_name = None + + if activity.isdigit() or activity == "-1": + _LOGGER.debug("%s: Activity is numeric", self.name) + activity_name = self._client.get_activity_name(int(activity)) + if activity_name: + activity_id = activity + + if activity_id is None: + _LOGGER.debug("%s: Find activity ID based on name", self.name) + activity_name = str(activity) + activity_id = self._client.get_activity_id(activity_name) + + if activity_id is None: + _LOGGER.error("%s: Activity %s is invalid", self.name, activity) + return + + _, current_activity_name = self.current_activity + if current_activity_name == activity_name: + # Automations or HomeKit may turn the device on multiple times + # when the current activity is already active which will cause + # harmony to loose state. This behavior is unexpected as turning + # the device on when its already on isn't expected to reset state. + _LOGGER.debug( + "%s: Current activity is already %s", self.name, activity_name + ) + return + + try: + await self._client.start_activity(activity_id) + except aioexc.TimeOut: + _LOGGER.error("%s: Starting activity %s timed-out", self.name, activity) + + async def async_power_off(self): + """Start the PowerOff activity.""" + _LOGGER.debug("%s: Turn Off", self.name) + try: + await self._client.power_off() + except aioexc.TimeOut: + _LOGGER.error("%s: Powering off timed-out", self.name) + + async def async_send_command( + self, + commands: Iterable[str], + device: str, + num_repeats: int, + delay_secs: float, + hold_secs: float, + ): + """Send a list of commands to one device.""" + device_id = None + if device.isdigit(): + _LOGGER.debug("%s: Device %s is numeric", self.name, device) + if self._client.get_device_name(int(device)): + device_id = device + + if device_id is None: + _LOGGER.debug( + "%s: Find device ID %s based on device name", self.name, device + ) + device_id = self._client.get_device_id(str(device).strip()) + + if device_id is None: + _LOGGER.error("%s: Device %s is invalid", self.name, device) + return + + _LOGGER.debug( + "Sending commands to device %s holding for %s seconds " + "with a delay of %s seconds", + device, + hold_secs, + delay_secs, + ) + + # Creating list of commands to send. + snd_cmnd_list = [] + for _ in range(num_repeats): + for single_command in commands: + send_command = SendCommandDevice( + device=device_id, command=single_command, delay=hold_secs + ) + snd_cmnd_list.append(send_command) + if delay_secs > 0: + snd_cmnd_list.append(float(delay_secs)) + + _LOGGER.debug("%s: Sending commands", self.name) + try: + result_list = await self._client.send_commands(snd_cmnd_list) + except aioexc.TimeOut: + _LOGGER.error("%s: Sending commands timed-out", self.name) + return + + for result in result_list: + _LOGGER.error( + "Sending command %s to device %s failed with code %s: %s", + result.command.command, + result.command.device, + result.code, + result.msg, + ) + + async def change_channel(self, channel: int): + """Change the channel using Harmony remote.""" + _LOGGER.debug("%s: Changing channel to %s", self.name, channel) + try: + await self._client.change_channel(channel) + except aioexc.TimeOut: + _LOGGER.error("%s: Changing channel to %s timed-out", self.name, channel) + + async def sync(self) -> bool: + """Sync the Harmony device with the web service. + + Returns True if the sync was successful. + """ + _LOGGER.debug("%s: Syncing hub with Harmony cloud", self.name) + try: + await self._client.sync() + except aioexc.TimeOut: + _LOGGER.error("%s: Syncing hub with Harmony cloud timed-out", self.name) + return False + else: + return True diff --git a/homeassistant/components/harmony/manifest.json b/homeassistant/components/harmony/manifest.json index 4d8b83f4643..7509f3d4f4d 100644 --- a/homeassistant/components/harmony/manifest.json +++ b/homeassistant/components/harmony/manifest.json @@ -3,12 +3,13 @@ "name": "Logitech Harmony Hub", "documentation": "https://www.home-assistant.io/integrations/harmony", "requirements": ["aioharmony==0.2.6"], - "codeowners": ["@ehendrix23", "@bramkragten", "@bdraco"], + "codeowners": ["@ehendrix23", "@bramkragten", "@bdraco", "@mkeesey"], "ssdp": [ { "manufacturer": "Logitech", "deviceType": "urn:myharmony-com:device:harmony:1" } ], + "dependencies": ["remote", "switch"], "config_flow": true } diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index ff7825013e8..8409983789b 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -1,11 +1,7 @@ """Support for Harmony Hub devices.""" -import asyncio import json import logging -from aioharmony.const import ClientCallbackType -import aioharmony.exceptions as aioexc -from aioharmony.harmonyapi import HarmonyAPI as HarmonyClient, SendCommandDevice import voluptuous as vol from homeassistant.components import remote @@ -16,17 +12,16 @@ from homeassistant.components.remote import ( ATTR_HOLD_SECS, ATTR_NUM_REPEATS, DEFAULT_DELAY_SECS, - PLATFORM_SCHEMA, ) -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant -from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.restore_state import RestoreEntity +from .connection_state import ConnectionStateMixin from .const import ( ACTIVITY_POWER_OFF, ATTR_ACTIVITY_LIST, @@ -39,15 +34,8 @@ from .const import ( PREVIOUS_ACTIVE_ACTIVITY, SERVICE_CHANGE_CHANNEL, SERVICE_SYNC, - UNIQUE_ID, -) -from .util import ( - find_best_name_for_remote, - find_matching_config_entries_for_host, - find_unique_id_for_remote, - get_harmony_client_if_available, - list_names_from_hublist, ) +from .subscriber import HarmonyCallback _LOGGER = logging.getLogger(__name__) @@ -56,18 +44,6 @@ PARALLEL_UPDATES = 0 ATTR_CHANNEL = "channel" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(ATTR_ACTIVITY): cv.string, - vol.Required(CONF_NAME): cv.string, - vol.Optional(ATTR_DELAY_SECS, default=DEFAULT_DELAY_SECS): vol.Coerce(float), - vol.Required(CONF_HOST): cv.string, - # The client ignores port so lets not confuse the user by pretenting we do anything with this - }, - extra=vol.ALLOW_EXTRA, -) - - HARMONY_SYNC_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) HARMONY_CHANGE_CHANNEL_SCHEMA = vol.Schema( @@ -78,45 +54,20 @@ HARMONY_CHANGE_CHANNEL_SCHEMA = vol.Schema( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Harmony platform.""" - - if discovery_info: - # Now handled by ssdp in the config flow - return - - if find_matching_config_entries_for_host(hass, config[CONF_HOST]): - return - - # We do the validation to verify we can connect - # so we can raise PlatformNotReady to force - # a retry so we can avoid a scenario where the config - # entry cannot be created via import because hub - # is not yet ready. - harmony = await get_harmony_client_if_available(config[CONF_HOST]) - if not harmony: - raise PlatformNotReady - - validated_config = config.copy() - validated_config[UNIQUE_ID] = find_unique_id_for_remote(harmony) - validated_config[CONF_NAME] = find_best_name_for_remote(config, harmony) - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=validated_config - ) - ) - - async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities ): """Set up the Harmony config entry.""" - device = hass.data[DOMAIN][entry.entry_id] + data = hass.data[DOMAIN][entry.entry_id] - _LOGGER.debug("Harmony Remote: %s", device) + _LOGGER.debug("HarmonyData : %s", data) + default_activity = entry.options.get(ATTR_ACTIVITY) + delay_secs = entry.options.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS) + + harmony_conf_file = hass.config.path(f"harmony_{entry.unique_id}.conf") + device = HarmonyRemote(data, default_activity, delay_secs, harmony_conf_file) async_add_entities([device]) platform = entity_platform.current_platform.get() @@ -131,37 +82,23 @@ async def async_setup_entry( ) -class HarmonyRemote(remote.RemoteEntity, RestoreEntity): +class HarmonyRemote(ConnectionStateMixin, remote.RemoteEntity, RestoreEntity): """Remote representation used to control a Harmony device.""" - def __init__(self, name, unique_id, host, activity, out_path, delay_secs): + def __init__(self, data, activity, delay_secs, out_path): """Initialize HarmonyRemote class.""" - self._name = name - self.host = host + super().__init__() + self._data = data + self._name = data.name self._state = None self._current_activity = ACTIVITY_POWER_OFF self.default_activity = activity self._activity_starting = None self._is_initial_update = True - self._client = HarmonyClient(ip_address=host) - self._config_path = out_path self.delay_secs = delay_secs - self._available = False - self._unique_id = unique_id + self._unique_id = data.unique_id self._last_activity = None - - @property - def activity_names(self): - """Names of all the remotes activities.""" - activities = [activity["label"] for activity in self._client.config["activity"]] - - # Remove both ways of representing PowerOff - if None in activities: - activities.remove(None) - if ACTIVITY_POWER_OFF in activities: - activities.remove(ACTIVITY_POWER_OFF) - - return activities + self._config_path = out_path async def _async_update_options(self, data): """Change options when the options flow does.""" @@ -171,15 +108,16 @@ class HarmonyRemote(remote.RemoteEntity, RestoreEntity): if ATTR_ACTIVITY in data: self.default_activity = data[ATTR_ACTIVITY] - def _update_callbacks(self): + def _setup_callbacks(self): callbacks = { + "connected": self.got_connected, + "disconnected": self.got_disconnected, "config_updated": self.new_config, - "connect": self.got_connected, - "disconnect": self.got_disconnected, - "new_activity_starting": self.new_activity, - "new_activity": self._new_activity_finished, + "activity_starting": self.new_activity, + "activity_started": self._new_activity_finished, } - self._client.callbacks = ClientCallbackType(**callbacks) + + self.async_on_remove(self._data.async_subscribe(HarmonyCallback(**callbacks))) def _new_activity_finished(self, activity_info: tuple) -> None: """Call for finished updated current activity.""" @@ -191,8 +129,9 @@ class HarmonyRemote(remote.RemoteEntity, RestoreEntity): await super().async_added_to_hass() _LOGGER.debug("%s: Harmony Hub added", self._name) - # Register the callbacks - self._update_callbacks() + + self.async_on_remove(self._clear_disconnection_delay) + self._setup_callbacks() self.async_on_remove( async_dispatcher_connect( @@ -219,29 +158,10 @@ class HarmonyRemote(remote.RemoteEntity, RestoreEntity): self._last_activity = last_state.attributes[ATTR_LAST_ACTIVITY] - async def shutdown(self): - """Close connection on shutdown.""" - _LOGGER.debug("%s: Closing Harmony Hub", self._name) - try: - await self._client.close() - except aioexc.TimeOut: - _LOGGER.warning("%s: Disconnect timed-out", self._name) - @property def device_info(self): """Return device info.""" - model = "Harmony Hub" - if "ethernetStatus" in self._client.hub_config.info: - model = "Harmony Hub Pro 2400" - return { - "identifiers": {(DOMAIN, self.unique_id)}, - "manufacturer": "Logitech", - "sw_version": self._client.hub_config.info.get( - "hubSwVersion", self._client.fw_version - ), - "name": self.name, - "model": model, - } + self._data.device_info(DOMAIN) @property def unique_id(self): @@ -264,10 +184,8 @@ class HarmonyRemote(remote.RemoteEntity, RestoreEntity): return { ATTR_ACTIVITY_STARTING: self._activity_starting, ATTR_CURRENT_ACTIVITY: self._current_activity, - ATTR_ACTIVITY_LIST: list_names_from_hublist( - self._client.hub_config.activities - ), - ATTR_DEVICES_LIST: list_names_from_hublist(self._client.hub_config.devices), + ATTR_ACTIVITY_LIST: self._data.activity_names, + ATTR_DEVICES_LIST: self._data.device_names, ATTR_LAST_ACTIVITY: self._last_activity, } @@ -279,20 +197,7 @@ class HarmonyRemote(remote.RemoteEntity, RestoreEntity): @property def available(self): """Return True if connected to Hub, otherwise False.""" - return self._available - - async def connect(self): - """Connect to the Harmony HUB.""" - _LOGGER.debug("%s: Connecting", self._name) - try: - if not await self._client.connect(): - _LOGGER.warning("%s: Unable to connect to HUB", self._name) - await self._client.close() - return False - except aioexc.TimeOut: - _LOGGER.warning("%s: Connection timed-out", self._name) - return False - return True + return self._data.available def new_activity(self, activity_info: tuple) -> None: """Call for updating the current activity.""" @@ -309,34 +214,14 @@ class HarmonyRemote(remote.RemoteEntity, RestoreEntity): # when turning on self._last_activity = activity_name self._state = bool(activity_id != -1) - self._available = True self.async_write_ha_state() async def new_config(self, _=None): """Call for updating the current activity.""" _LOGGER.debug("%s: configuration has been updated", self._name) - self.new_activity(self._client.current_activity) + self.new_activity(self._data.current_activity) await self.hass.async_add_executor_job(self.write_config_file) - async def got_connected(self, _=None): - """Notification that we're connected to the HUB.""" - _LOGGER.debug("%s: connected to the HUB", self._name) - if not self._available: - # We were disconnected before. - await self.new_config() - - async def got_disconnected(self, _=None): - """Notification that we're disconnected from the HUB.""" - _LOGGER.debug("%s: disconnected from the HUB", self._name) - self._available = False - # We're going to wait for 10 seconds before announcing we're - # unavailable, this to allow a reconnection to happen. - await asyncio.sleep(10) - - if not self._available: - # Still disconnected. Let the state engine know. - self.async_write_ha_state() - async def async_turn_on(self, **kwargs): """Start an activity from the Harmony device.""" _LOGGER.debug("%s: Turn On", self.name) @@ -347,55 +232,18 @@ class HarmonyRemote(remote.RemoteEntity, RestoreEntity): if self._last_activity: activity = self._last_activity else: - all_activities = list_names_from_hublist( - self._client.hub_config.activities - ) + all_activities = self._data.activity_names if all_activities: activity = all_activities[0] if activity: - activity_id = None - activity_name = None - - if activity.isdigit() or activity == "-1": - _LOGGER.debug("%s: Activity is numeric", self.name) - activity_name = self._client.get_activity_name(int(activity)) - if activity_name: - activity_id = activity - - if activity_id is None: - _LOGGER.debug("%s: Find activity ID based on name", self.name) - activity_name = str(activity) - activity_id = self._client.get_activity_id(activity_name) - - if activity_id is None: - _LOGGER.error("%s: Activity %s is invalid", self.name, activity) - return - - if self._current_activity == activity_name: - # Automations or HomeKit may turn the device on multiple times - # when the current activity is already active which will cause - # harmony to loose state. This behavior is unexpected as turning - # the device on when its already on isn't expected to reset state. - _LOGGER.debug( - "%s: Current activity is already %s", self.name, activity_name - ) - return - - try: - await self._client.start_activity(activity_id) - except aioexc.TimeOut: - _LOGGER.error("%s: Starting activity %s timed-out", self.name, activity) + await self._data.async_start_activity(activity) else: _LOGGER.error("%s: No activity specified with turn_on service", self.name) async def async_turn_off(self, **kwargs): """Start the PowerOff activity.""" - _LOGGER.debug("%s: Turn Off", self.name) - try: - await self._client.power_off() - except aioexc.TimeOut: - _LOGGER.error("%s: Powering off timed-out", self.name) + await self._data.async_power_off() async def async_send_command(self, command, **kwargs): """Send a list of commands to one device.""" @@ -405,90 +253,38 @@ class HarmonyRemote(remote.RemoteEntity, RestoreEntity): _LOGGER.error("%s: Missing required argument: device", self.name) return - device_id = None - if device.isdigit(): - _LOGGER.debug("%s: Device %s is numeric", self.name, device) - if self._client.get_device_name(int(device)): - device_id = device - - if device_id is None: - _LOGGER.debug( - "%s: Find device ID %s based on device name", self.name, device - ) - device_id = self._client.get_device_id(str(device).strip()) - - if device_id is None: - _LOGGER.error("%s: Device %s is invalid", self.name, device) - return - num_repeats = kwargs[ATTR_NUM_REPEATS] delay_secs = kwargs.get(ATTR_DELAY_SECS, self.delay_secs) hold_secs = kwargs[ATTR_HOLD_SECS] - _LOGGER.debug( - "Sending commands to device %s holding for %s seconds " - "with a delay of %s seconds", - device, - hold_secs, - delay_secs, + await self._data.async_send_command( + command, device, num_repeats, delay_secs, hold_secs ) - # Creating list of commands to send. - snd_cmnd_list = [] - for _ in range(num_repeats): - for single_command in command: - send_command = SendCommandDevice( - device=device_id, command=single_command, delay=hold_secs - ) - snd_cmnd_list.append(send_command) - if delay_secs > 0: - snd_cmnd_list.append(float(delay_secs)) - - _LOGGER.debug("%s: Sending commands", self.name) - try: - result_list = await self._client.send_commands(snd_cmnd_list) - except aioexc.TimeOut: - _LOGGER.error("%s: Sending commands timed-out", self.name) - return - - for result in result_list: - _LOGGER.error( - "Sending command %s to device %s failed with code %s: %s", - result.command.command, - result.command.device, - result.code, - result.msg, - ) - async def change_channel(self, channel): """Change the channel using Harmony remote.""" - _LOGGER.debug("%s: Changing channel to %s", self.name, channel) - try: - await self._client.change_channel(channel) - except aioexc.TimeOut: - _LOGGER.error("%s: Changing channel to %s timed-out", self.name, channel) + await self._data.change_channel(channel) async def sync(self): """Sync the Harmony device with the web service.""" - _LOGGER.debug("%s: Syncing hub with Harmony cloud", self.name) - try: - await self._client.sync() - except aioexc.TimeOut: - _LOGGER.error("%s: Syncing hub with Harmony cloud timed-out", self.name) - else: + if await self._data.sync(): await self.hass.async_add_executor_job(self.write_config_file) def write_config_file(self): - """Write Harmony configuration file.""" + """Write Harmony configuration file. + + This is a handy way for users to figure out the available commands for automations. + """ _LOGGER.debug( "%s: Writing hub configuration to file: %s", self.name, self._config_path ) - if self._client.config is None: + json_config = self._data.json_config + if json_config is None: _LOGGER.warning("%s: No configuration received from hub", self.name) return try: with open(self._config_path, "w+", encoding="utf-8") as file_out: - json.dump(self._client.json_config, file_out, sort_keys=True, indent=4) + json.dump(json_config, file_out, sort_keys=True, indent=4) except OSError as exc: _LOGGER.error( "%s: Unable to write HUB configuration to %s: %s", diff --git a/homeassistant/components/harmony/subscriber.py b/homeassistant/components/harmony/subscriber.py new file mode 100644 index 00000000000..d3bed33d560 --- /dev/null +++ b/homeassistant/components/harmony/subscriber.py @@ -0,0 +1,77 @@ +"""Mixin class for handling harmony callback subscriptions.""" + +import logging +from typing import Any, Callable, NamedTuple, Optional + +from homeassistant.core import callback + +_LOGGER = logging.getLogger(__name__) + +NoParamCallback = Optional[Callable[[object], Any]] +ActivityCallback = Optional[Callable[[object, tuple], Any]] + + +class HarmonyCallback(NamedTuple): + """Callback type for Harmony Hub notifications.""" + + connected: NoParamCallback + disconnected: NoParamCallback + config_updated: NoParamCallback + activity_starting: ActivityCallback + activity_started: ActivityCallback + + +class HarmonySubscriberMixin: + """Base implementation for a subscriber.""" + + def __init__(self, hass): + """Initialize an subscriber.""" + super().__init__() + self._hass = hass + self._subscriptions = [] + + @callback + def async_subscribe(self, update_callbacks: HarmonyCallback) -> Callable: + """Add a callback subscriber.""" + self._subscriptions.append(update_callbacks) + + def _unsubscribe(): + self.async_unsubscribe(update_callbacks) + + return _unsubscribe + + @callback + def async_unsubscribe(self, update_callback: HarmonyCallback): + """Remove a callback subscriber.""" + self._subscriptions.remove(update_callback) + + def _config_updated(self, _=None) -> None: + _LOGGER.debug("config_updated") + self._call_callbacks("config_updated") + + def _connected(self, _=None) -> None: + _LOGGER.debug("connected") + self._available = True + self._call_callbacks("connected") + + def _disconnected(self, _=None) -> None: + _LOGGER.debug("disconnected") + self._available = False + self._call_callbacks("disconnected") + + def _activity_starting(self, activity_info: tuple) -> None: + _LOGGER.debug("activity %s starting", activity_info) + self._call_callbacks("activity_starting", activity_info) + + def _activity_started(self, activity_info: tuple) -> None: + _LOGGER.debug("activity %s started", activity_info) + self._call_callbacks("activity_started", activity_info) + + def _call_callbacks(self, callback_func_name: str, argument: tuple = None): + for subscription in self._subscriptions: + current_callback = getattr(subscription, callback_func_name) + if current_callback: + if argument: + self._hass.async_run_job(current_callback, argument) + else: + self._hass.async_run_job(current_callback) diff --git a/homeassistant/components/harmony/switch.py b/homeassistant/components/harmony/switch.py new file mode 100644 index 00000000000..5fae07c431b --- /dev/null +++ b/homeassistant/components/harmony/switch.py @@ -0,0 +1,87 @@ +"""Support for Harmony Hub activities.""" +import logging + +from homeassistant.components.switch import SwitchEntity +from homeassistant.const import CONF_NAME + +from .connection_state import ConnectionStateMixin +from .const import DOMAIN +from .data import HarmonyData +from .subscriber import HarmonyCallback + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up harmony activity switches.""" + data = hass.data[DOMAIN][entry.entry_id] + activities = data.activity_names + + switches = [] + for activity in activities: + _LOGGER.debug("creating switch for activity: %s", activity) + name = f"{entry.data[CONF_NAME]} {activity}" + switches.append(HarmonyActivitySwitch(name, activity, data)) + + async_add_entities(switches, True) + + +class HarmonyActivitySwitch(ConnectionStateMixin, SwitchEntity): + """Switch representation of a Harmony activity.""" + + def __init__(self, name: str, activity: str, data: HarmonyData): + """Initialize HarmonyActivitySwitch class.""" + super().__init__() + self._name = name + self._activity = activity + self._data = data + + @property + def name(self): + """Return the Harmony activity's name.""" + return self._name + + @property + def unique_id(self): + """Return the unique id.""" + return f"{self._data.unique_id}-{self._activity}" + + @property + def is_on(self): + """Return if the current activity is the one for this switch.""" + _, activity_name = self._data.current_activity + return activity_name == self._activity + + @property + def should_poll(self): + """Return that we shouldn't be polled.""" + return False + + @property + def available(self): + """Return True if we're connected to the Hub, otherwise False.""" + return self._data.available + + async def async_turn_on(self, **kwargs): + """Start this activity.""" + await self._data.async_start_activity(self._activity) + + async def async_turn_off(self, **kwargs): + """Stop this activity.""" + await self._data.async_power_off() + + async def async_added_to_hass(self): + """Call when entity is added to hass.""" + + callbacks = { + "connected": self.got_connected, + "disconnected": self.got_disconnected, + "activity_starting": self._activity_update, + "activity_started": self._activity_update, + "config_updated": None, + } + + self.async_on_remove(self._data.async_subscribe(HarmonyCallback(**callbacks))) + + def _activity_update(self, activity_info: tuple): + self.async_write_ha_state() diff --git a/homeassistant/components/harmony/translations/de.json b/homeassistant/components/harmony/translations/de.json index f10dfe1432c..9cd07f09529 100644 --- a/homeassistant/components/harmony/translations/de.json +++ b/homeassistant/components/harmony/translations/de.json @@ -4,7 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler" }, "flow_title": "Logitech Harmony Hub {name}", diff --git a/homeassistant/components/harmony/translations/tr.json b/homeassistant/components/harmony/translations/tr.json new file mode 100644 index 00000000000..c77f0f8e07e --- /dev/null +++ b/homeassistant/components/harmony/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/uk.json b/homeassistant/components/harmony/translations/uk.json new file mode 100644 index 00000000000..5bb2da811f3 --- /dev/null +++ b/homeassistant/components/harmony/translations/uk.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "flow_title": "Logitech Harmony Hub {name}", + "step": { + "link": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name} ({host})?", + "title": "Logitech Harmony Hub" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "title": "Logitech Harmony Hub" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "activity": "\u0410\u043a\u0442\u0438\u0432\u043d\u0456\u0441\u0442\u044c \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c, \u043a\u043e\u043b\u0438 \u0436\u043e\u0434\u043d\u0430 \u0437 \u043d\u0438\u0445 \u043d\u0435 \u0432\u043a\u0430\u0437\u0430\u043d\u0430.", + "delay_secs": "\u0417\u0430\u0442\u0440\u0438\u043c\u043a\u0430 \u043c\u0456\u0436 \u043d\u0430\u0434\u0441\u0438\u043b\u0430\u043d\u043d\u044f\u043c \u043a\u043e\u043c\u0430\u043d\u0434." + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432 Harmony Hub" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/util.py b/homeassistant/components/harmony/util.py index b0a16004065..3f126f22f3c 100644 --- a/homeassistant/components/harmony/util.py +++ b/homeassistant/components/harmony/util.py @@ -2,9 +2,7 @@ import aioharmony.exceptions as harmony_exceptions from aioharmony.harmonyapi import HarmonyAPI -from homeassistant.const import CONF_HOST, CONF_NAME - -from .const import DOMAIN +from homeassistant.const import CONF_NAME def find_unique_id_for_remote(harmony: HarmonyAPI): @@ -41,22 +39,3 @@ async def get_harmony_client_if_available(ip_address: str): await harmony.close() return harmony - - -def find_matching_config_entries_for_host(hass, host): - """Search existing config entries for one matching the host.""" - for entry in hass.config_entries.async_entries(DOMAIN): - if entry.data[CONF_HOST] == host: - return entry - return None - - -def list_names_from_hublist(hub_list): - """Extract the name key value from a hub list of names.""" - if not hub_list: - return [] - return [ - element["name"] - for element in hub_list - if element.get("name") and element.get("id") != -1 - ] diff --git a/homeassistant/components/hassio/system_health.py b/homeassistant/components/hassio/system_health.py index 530703d3e25..47e9a3d2995 100644 --- a/homeassistant/components/hassio/system_health.py +++ b/homeassistant/components/hassio/system_health.py @@ -43,7 +43,7 @@ async def system_health_info(hass: HomeAssistant): information = { "host_os": host_info.get("operating_system"), "update_channel": info.get("channel"), - "supervisor_version": info.get("supervisor"), + "supervisor_version": f"supervisor-{info.get('supervisor')}", "docker_version": info.get("docker"), "disk_total": f"{host_info.get('disk_total')} GB", "disk_used": f"{host_info.get('disk_used')} GB", diff --git a/homeassistant/components/hassio/translations/ca.json b/homeassistant/components/hassio/translations/ca.json index 0cdb9318428..19b4316c9ce 100644 --- a/homeassistant/components/hassio/translations/ca.json +++ b/homeassistant/components/hassio/translations/ca.json @@ -12,7 +12,7 @@ "supervisor_version": "Versi\u00f3 del Supervisor", "supported": "Compatible", "update_channel": "Canal d'actualitzaci\u00f3", - "version_api": "API de versions" + "version_api": "Versi\u00f3 d'APIs" } }, "title": "Hass.io" diff --git a/homeassistant/components/hassio/translations/de.json b/homeassistant/components/hassio/translations/de.json index 981cb51c83a..939821edb54 100644 --- a/homeassistant/components/hassio/translations/de.json +++ b/homeassistant/components/hassio/translations/de.json @@ -1,3 +1,18 @@ { + "system_health": { + "info": { + "board": "Board", + "disk_total": "Speicherplatz gesamt", + "disk_used": "Speicherplatz genutzt", + "docker_version": "Docker-Version", + "host_os": "Host-Betriebssystem", + "installed_addons": "Installierte Add-ons", + "supervisor_api": "Supervisor-API", + "supervisor_version": "Supervisor-Version", + "supported": "Unterst\u00fctzt", + "update_channel": "Update-Channel", + "version_api": "Versions-API" + } + }, "title": "Hass.io" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/fr.json b/homeassistant/components/hassio/translations/fr.json index 981cb51c83a..2bb52c3c54c 100644 --- a/homeassistant/components/hassio/translations/fr.json +++ b/homeassistant/components/hassio/translations/fr.json @@ -1,3 +1,18 @@ { + "system_health": { + "info": { + "board": "Tableau de bord", + "disk_total": "Taille total du disque", + "disk_used": "Taille du disque utilis\u00e9", + "docker_version": "Version de Docker", + "healthy": "Sain", + "installed_addons": "Add-ons install\u00e9s", + "supervisor_api": "API du superviseur", + "supervisor_version": "Version du supervisor", + "supported": "Prise en charge", + "update_channel": "Mise \u00e0 jour", + "version_api": "Version API" + } + }, "title": "Hass.io" } \ No newline at end of file diff --git a/homeassistant/components/hassio/translations/tr.json b/homeassistant/components/hassio/translations/tr.json index d368ac0fb3c..f2c2d52f60d 100644 --- a/homeassistant/components/hassio/translations/tr.json +++ b/homeassistant/components/hassio/translations/tr.json @@ -6,7 +6,13 @@ "disk_used": "Kullan\u0131lan Disk", "docker_version": "Docker S\u00fcr\u00fcm\u00fc", "healthy": "Sa\u011fl\u0131kl\u0131", - "host_os": "Ana Bilgisayar \u0130\u015fletim Sistemi" + "host_os": "Ana Bilgisayar \u0130\u015fletim Sistemi", + "installed_addons": "Y\u00fckl\u00fc Eklentiler", + "supervisor_api": "Supervisor API", + "supervisor_version": "S\u00fcperviz\u00f6r S\u00fcr\u00fcm\u00fc", + "supported": "Destekleniyor", + "update_channel": "Kanal\u0131 G\u00fcncelle", + "version_api": "S\u00fcr\u00fcm API" } }, "title": "Hass.io" diff --git a/homeassistant/components/hassio/translations/uk.json b/homeassistant/components/hassio/translations/uk.json index 981cb51c83a..19a40730897 100644 --- a/homeassistant/components/hassio/translations/uk.json +++ b/homeassistant/components/hassio/translations/uk.json @@ -1,3 +1,19 @@ { + "system_health": { + "info": { + "board": "\u041f\u043b\u0430\u0442\u0430", + "disk_total": "\u0414\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u043f\u0430\u043c'\u044f\u0442\u044c", + "disk_used": "\u041f\u0430\u043c'\u044f\u0442\u0456 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043e", + "docker_version": "\u0412\u0435\u0440\u0441\u0456\u044f Docker", + "healthy": "\u0412 \u043d\u043e\u0440\u043c\u0456", + "host_os": "\u041e\u043f\u0435\u0440\u0430\u0446\u0456\u0439\u043d\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u0445\u043e\u0441\u0442\u0430", + "installed_addons": "\u0412\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0456 \u0434\u043e\u043f\u043e\u0432\u043d\u0435\u043d\u043d\u044f", + "supervisor_api": "Supervisor API", + "supervisor_version": "\u0412\u0435\u0440\u0441\u0456\u044f Supervisor", + "supported": "\u041f\u0456\u0434\u0442\u0440\u0438\u043c\u043a\u0430", + "update_channel": "\u041a\u0430\u043d\u0430\u043b \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u044c", + "version_api": "\u0412\u0435\u0440\u0441\u0456\u044f API" + } + }, "title": "Hass.io" } \ No newline at end of file diff --git a/homeassistant/components/heos/__init__.py b/homeassistant/components/heos/__init__.py index 81f9fd9dd0e..11020d1166e 100644 --- a/homeassistant/components/heos/__init__.py +++ b/homeassistant/components/heos/__init__.py @@ -196,7 +196,7 @@ class ControllerManager: # mapped_ids contains the mapped IDs (new:old) for new_id, old_id in mapped_ids.items(): # update device registry - entry = self._device_registry.async_get_device({(DOMAIN, old_id)}, set()) + entry = self._device_registry.async_get_device({(DOMAIN, old_id)}) new_identifiers = {(DOMAIN, new_id)} if entry: self._device_registry.async_update_device( diff --git a/homeassistant/components/heos/translations/de.json b/homeassistant/components/heos/translations/de.json index 92ab6c1c8ff..ba8a5318951 100644 --- a/homeassistant/components/heos/translations/de.json +++ b/homeassistant/components/heos/translations/de.json @@ -1,7 +1,10 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "user": { diff --git a/homeassistant/components/heos/translations/tr.json b/homeassistant/components/heos/translations/tr.json new file mode 100644 index 00000000000..4f1ad775905 --- /dev/null +++ b/homeassistant/components/heos/translations/tr.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/heos/translations/uk.json b/homeassistant/components/heos/translations/uk.json new file mode 100644 index 00000000000..c0a5fdf04bf --- /dev/null +++ b/homeassistant/components/heos/translations/uk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "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." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0432\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043c'\u044f \u0445\u043e\u0441\u0442\u0430 \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e HEOS (\u0431\u0430\u0436\u0430\u043d\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u0434\u043e \u043c\u0435\u0440\u0435\u0436\u0456 \u0447\u0435\u0440\u0435\u0437 \u043a\u0430\u0431\u0435\u043b\u044c).", + "title": "HEOS" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/translations/de.json b/homeassistant/components/hisense_aehw4a1/translations/de.json index d5f4f429740..7c0bd96a9c9 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/de.json +++ b/homeassistant/components/hisense_aehw4a1/translations/de.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Es wurden keine Hisense AEH-W4A1-Ger\u00e4te im Netzwerk gefunden.", - "single_instance_allowed": "Es ist nur eine einzige Konfiguration von Hisense AEH-W4A1 m\u00f6glich." + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "step": { "confirm": { diff --git a/homeassistant/components/hisense_aehw4a1/translations/tr.json b/homeassistant/components/hisense_aehw4a1/translations/tr.json new file mode 100644 index 00000000000..a893a653a78 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "confirm": { + "description": "Hisense AEH-W4A1'i kurmak istiyor musunuz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/translations/uk.json b/homeassistant/components/hisense_aehw4a1/translations/uk.json new file mode 100644 index 00000000000..900882513d5 --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/translations/uk.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "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." + }, + "step": { + "confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Hisense AEH-W4A1?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 894c2b15e47..1e22e45a892 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -699,7 +699,7 @@ class LazyState(State): """Init the lazy state.""" self._row = row self.entity_id = self._row.entity_id - self.state = self._row.state + self.state = self._row.state or "" self._attributes = None self._last_changed = None self._last_updated = None diff --git a/homeassistant/components/hlk_sw16/translations/de.json b/homeassistant/components/hlk_sw16/translations/de.json index 94b8d6526d1..625c7372347 100644 --- a/homeassistant/components/hlk_sw16/translations/de.json +++ b/homeassistant/components/hlk_sw16/translations/de.json @@ -1,11 +1,17 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { "user": { "data": { + "host": "Host", "password": "Passwort", "username": "Benutzername" } diff --git a/homeassistant/components/hlk_sw16/translations/tr.json b/homeassistant/components/hlk_sw16/translations/tr.json new file mode 100644 index 00000000000..40c9c39b967 --- /dev/null +++ b/homeassistant/components/hlk_sw16/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hlk_sw16/translations/uk.json b/homeassistant/components/hlk_sw16/translations/uk.json new file mode 100644 index 00000000000..2df11f74455 --- /dev/null +++ b/homeassistant/components/hlk_sw16/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/home_connect/light.py b/homeassistant/components/home_connect/light.py index 56b0dd39fe5..814e3b0ed03 100644 --- a/homeassistant/components/home_connect/light.py +++ b/homeassistant/components/home_connect/light.py @@ -90,6 +90,16 @@ class HomeConnectLight(HomeConnectEntity, LightEntity): async def async_turn_on(self, **kwargs): """Switch the light on, change brightness, change color.""" if self._ambient: + _LOGGER.debug("Switching ambient light on for: %s", self.name) + try: + await self.hass.async_add_executor_job( + self.device.appliance.set_setting, + self._key, + True, + ) + except HomeConnectError as err: + _LOGGER.error("Error while trying to turn on ambient light: %s", err) + return if ATTR_BRIGHTNESS in kwargs or ATTR_HS_COLOR in kwargs: try: await self.hass.async_add_executor_job( @@ -119,18 +129,6 @@ class HomeConnectLight(HomeConnectEntity, LightEntity): _LOGGER.error( "Error while trying setting the color: %s", err ) - else: - _LOGGER.debug("Switching ambient light on for: %s", self.name) - try: - await self.hass.async_add_executor_job( - self.device.appliance.set_setting, - self._key, - True, - ) - except HomeConnectError as err: - _LOGGER.error( - "Error while trying to turn on ambient light: %s", err - ) elif ATTR_BRIGHTNESS in kwargs: _LOGGER.debug("Changing brightness for: %s", self.name) diff --git a/homeassistant/components/home_connect/translations/de.json b/homeassistant/components/home_connect/translations/de.json index 05204c35c41..2454c039361 100644 --- a/homeassistant/components/home_connect/translations/de.json +++ b/homeassistant/components/home_connect/translations/de.json @@ -1,14 +1,15 @@ { "config": { "abort": { - "missing_configuration": "Die Komponente Home Connect ist nicht konfiguriert. Bitte folgen Sie der Dokumentation." + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url})." }, "create_entry": { - "default": "Erfolgreich mit Home Connect authentifiziert." + "default": "Erfolgreich authentifiziert" }, "step": { "pick_implementation": { - "title": "Authentifizierungsmethode ausw\u00e4hlen" + "title": "W\u00e4hle die Authentifizierungsmethode" } } } diff --git a/homeassistant/components/home_connect/translations/uk.json b/homeassistant/components/home_connect/translations/uk.json new file mode 100644 index 00000000000..247ffd16713 --- /dev/null +++ b/homeassistant/components/home_connect/translations/uk.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443." + }, + "create_entry": { + "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." + }, + "step": { + "pick_implementation": { + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/system_health.py b/homeassistant/components/homeassistant/system_health.py index b0245d9beec..ff3562a24f9 100644 --- a/homeassistant/components/homeassistant/system_health.py +++ b/homeassistant/components/homeassistant/system_health.py @@ -17,7 +17,7 @@ async def system_health_info(hass): info = await system_info.async_get_system_info(hass) return { - "version": info.get("version"), + "version": f"core-{info.get('version')}", "installation_type": info.get("installation_type"), "dev": info.get("dev"), "hassio": info.get("hassio"), diff --git a/homeassistant/components/homeassistant/translations/de.json b/homeassistant/components/homeassistant/translations/de.json index 45768a9f127..e24568ff212 100644 --- a/homeassistant/components/homeassistant/translations/de.json +++ b/homeassistant/components/homeassistant/translations/de.json @@ -1,10 +1,21 @@ { "system_health": { "info": { + "arch": "CPU-Architektur", + "chassis": "Chassis", + "dev": "Entwicklung", "docker": "Docker", "docker_version": "Docker", "hassio": "Supervisor", - "host_os": "Home Assistant OS" + "host_os": "Home Assistant OS", + "installation_type": "Installationstyp", + "os_name": "Betriebssystemfamilie", + "os_version": "Betriebssystem-Version", + "python_version": "Python-Version", + "supervisor": "Supervisor", + "timezone": "Zeitzone", + "version": "Version", + "virtualenv": "Virtuelle Umgebung" } } } \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/fr.json b/homeassistant/components/homeassistant/translations/fr.json new file mode 100644 index 00000000000..194254a0384 --- /dev/null +++ b/homeassistant/components/homeassistant/translations/fr.json @@ -0,0 +1,21 @@ +{ + "system_health": { + "info": { + "arch": "Architecture du processeur", + "chassis": "Ch\u00e2ssis", + "dev": "D\u00e9veloppement", + "docker": "Docker", + "docker_version": "Docker", + "hassio": "Superviseur", + "host_os": "Home Assistant OS", + "installation_type": "Type d'installation", + "os_name": "Famille du syst\u00e8me d'exploitation", + "os_version": "Version du syst\u00e8me d'exploitation", + "python_version": "Version de Python", + "supervisor": "Supervisor", + "timezone": "Fuseau horaire", + "version": "Version", + "virtualenv": "Environnement virtuel" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/translations/tr.json b/homeassistant/components/homeassistant/translations/tr.json index 1ff8ea1b3a9..c2b7ca1b10c 100644 --- a/homeassistant/components/homeassistant/translations/tr.json +++ b/homeassistant/components/homeassistant/translations/tr.json @@ -2,9 +2,11 @@ "system_health": { "info": { "arch": "CPU Mimarisi", + "chassis": "Ana G\u00f6vde", "dev": "Geli\u015ftirme", "docker": "Konteyner", "docker_version": "Konteyner", + "hassio": "S\u00fcperviz\u00f6r", "host_os": "Home Assistant OS", "installation_type": "Kurulum T\u00fcr\u00fc", "os_name": "\u0130\u015fletim Sistemi Ailesi", diff --git a/homeassistant/components/homeassistant/translations/uk.json b/homeassistant/components/homeassistant/translations/uk.json new file mode 100644 index 00000000000..19e07c8f822 --- /dev/null +++ b/homeassistant/components/homeassistant/translations/uk.json @@ -0,0 +1,21 @@ +{ + "system_health": { + "info": { + "arch": "\u0410\u0440\u0445\u0456\u0442\u0435\u043a\u0442\u0443\u0440\u0430 \u0426\u041f", + "chassis": "\u0428\u0430\u0441\u0456", + "dev": "\u0421\u0435\u0440\u0435\u0434\u043e\u0432\u0438\u0449\u0435 \u0440\u043e\u0437\u0440\u043e\u0431\u043a\u0438", + "docker": "Docker", + "docker_version": "Docker", + "hassio": "Supervisor", + "host_os": "Home Assistant OS", + "installation_type": "\u0422\u0438\u043f \u0456\u043d\u0441\u0442\u0430\u043b\u044f\u0446\u0456\u0457", + "os_name": "\u0421\u0456\u043c\u0435\u0439\u0441\u0442\u0432\u043e \u043e\u043f\u0435\u0440\u0430\u0446\u0456\u0439\u043d\u0438\u0445 \u0441\u0438\u0441\u0442\u0435\u043c", + "os_version": "\u0412\u0435\u0440\u0441\u0456\u044f \u043e\u043f\u0435\u0440\u0430\u0446\u0456\u0439\u043d\u043e\u0457 \u0441\u0438\u0441\u0442\u0435\u043c\u0438", + "python_version": "\u0412\u0435\u0440\u0441\u0456\u044f Python", + "supervisor": "Supervisor", + "timezone": "\u0427\u0430\u0441\u043e\u0432\u0438\u0439 \u043f\u043e\u044f\u0441", + "version": "\u0412\u0435\u0440\u0441\u0456\u044f", + "virtualenv": "\u0412\u0456\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u0435 \u043e\u0442\u043e\u0447\u0435\u043d\u043d\u044f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homeassistant/triggers/numeric_state.py b/homeassistant/components/homeassistant/triggers/numeric_state.py index 25b9a4417dc..7cfee8fad93 100644 --- a/homeassistant/components/homeassistant/triggers/numeric_state.py +++ b/homeassistant/components/homeassistant/triggers/numeric_state.py @@ -32,6 +32,9 @@ def validate_above_below(value): if above is None or below is None: return value + if isinstance(above, str) or isinstance(below, str): + return value + if above > below: raise vol.Invalid( f"A value can never be above {above} and below {below} at the same time. You probably want two different triggers.", @@ -45,8 +48,8 @@ TRIGGER_SCHEMA = vol.All( { vol.Required(CONF_PLATFORM): "numeric_state", vol.Required(CONF_ENTITY_ID): cv.entity_ids, - vol.Optional(CONF_BELOW): vol.Coerce(float), - vol.Optional(CONF_ABOVE): vol.Coerce(float), + vol.Optional(CONF_BELOW): cv.NUMERIC_STATE_THRESHOLD_SCHEMA, + vol.Optional(CONF_ABOVE): cv.NUMERIC_STATE_THRESHOLD_SCHEMA, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_FOR): cv.positive_time_period_template, vol.Optional(CONF_ATTRIBUTE): cv.match_all, diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 1155d3ef18e..53fbd7cf8f1 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -5,7 +5,7 @@ import logging import os from aiohttp import web -from pyhap.const import STANDALONE_AID +from pyhap.const import CATEGORY_CAMERA, CATEGORY_TELEVISION, STANDALONE_AID import voluptuous as vol from homeassistant.components import zeroconf @@ -114,6 +114,7 @@ def _has_all_unique_names_and_ports(bridges): BRIDGE_SCHEMA = vol.All( cv.deprecated(CONF_ZEROCONF_DEFAULT_INTERFACE), + cv.deprecated(CONF_SAFE_MODE), vol.Schema( { vol.Optional(CONF_HOMEKIT_MODE, default=DEFAULT_HOMEKIT_MODE): vol.In( @@ -246,7 +247,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): homekit_mode = options.get(CONF_HOMEKIT_MODE, DEFAULT_HOMEKIT_MODE) entity_config = options.get(CONF_ENTITY_CONFIG, {}).copy() auto_start = options.get(CONF_AUTO_START, DEFAULT_AUTO_START) - safe_mode = options.get(CONF_SAFE_MODE, DEFAULT_SAFE_MODE) entity_filter = FILTER_SCHEMA(options.get(CONF_FILTER, {})) homekit = HomeKit( @@ -256,7 +256,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ip_address, entity_filter, entity_config, - safe_mode, homekit_mode, advertise_ip, entry.entry_id, @@ -421,7 +420,6 @@ class HomeKit: ip_address, entity_filter, entity_config, - safe_mode, homekit_mode, advertise_ip=None, entry_id=None, @@ -433,7 +431,6 @@ class HomeKit: self._ip_address = ip_address self._filter = entity_filter self._config = entity_config - self._safe_mode = safe_mode self._advertise_ip = advertise_ip self._entry_id = entry_id self._homekit_mode = homekit_mode @@ -470,10 +467,6 @@ class HomeKit: else: self.driver.persist() - if self._safe_mode: - _LOGGER.debug("Safe_mode selected for %s", self._name) - self.driver.safe_mode = True - def reset_accessories(self, entity_ids): """Reset the accessory to load the latest configuration.""" if not self.bridge: @@ -530,6 +523,24 @@ class HomeKit: try: acc = get_accessory(self.hass, self.driver, state, aid, conf) if acc is not None: + if acc.category == CATEGORY_CAMERA: + _LOGGER.warning( + "The bridge %s has camera %s. For best performance, " + "and to prevent unexpected unavailability, create and " + "pair a separate HomeKit instance in accessory mode for " + "each camera.", + self._name, + acc.entity_id, + ) + elif acc.category == CATEGORY_TELEVISION: + _LOGGER.warning( + "The bridge %s has tv %s. For best performance, " + "and to prevent unexpected unavailability, create and " + "pair a separate HomeKit instance in accessory mode for " + "each tv media player.", + self._name, + acc.entity_id, + ) self.bridge.add_accessory(acc) except Exception: # pylint: disable=broad-except _LOGGER.exception( diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index 6dc2e2364b6..51b6508149b 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -8,7 +8,7 @@ from pyhap.accessory import Accessory, Bridge from pyhap.accessory_driver import AccessoryDriver from pyhap.const import CATEGORY_OTHER -from homeassistant.components import cover, vacuum +from homeassistant.components import cover from homeassistant.components.cover import ( DEVICE_CLASS_GARAGE, DEVICE_CLASS_GATE, @@ -215,11 +215,7 @@ def get_accessory(hass, driver, state, aid, config): a_type = SWITCH_TYPES[switch_type] elif state.domain == "vacuum": - features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) - if features & (vacuum.SUPPORT_START | vacuum.SUPPORT_RETURN_HOME): - a_type = "DockVacuum" - else: - a_type = "Switch" + a_type = "Vacuum" elif state.domain in ("automation", "input_boolean", "remote", "scene", "script"): a_type = "Switch" diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index 9d50e62fcd1..8d763581615 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -21,12 +21,10 @@ from .const import ( CONF_ENTITY_CONFIG, CONF_FILTER, CONF_HOMEKIT_MODE, - CONF_SAFE_MODE, CONF_VIDEO_CODEC, DEFAULT_AUTO_START, DEFAULT_CONFIG_FLOW_PORT, DEFAULT_HOMEKIT_MODE, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_ACCESSORY, HOMEKIT_MODES, SHORT_BRIDGE_NAME, @@ -217,40 +215,32 @@ class OptionsFlowHandler(config_entries.OptionsFlow): async def async_step_advanced(self, user_input=None): """Choose advanced options.""" - if user_input is not None: - self.homekit_options.update(user_input) - for key in (CONF_DOMAINS, CONF_ENTITIES): - if key in self.homekit_options: - del self.homekit_options[key] - return self.async_create_entry(title="", data=self.homekit_options) + if not self.show_advanced_options or user_input is not None: + if user_input: + self.homekit_options.update(user_input) - schema_base = {} - - if self.show_advanced_options: - schema_base[ - vol.Optional( - CONF_AUTO_START, - default=self.homekit_options.get( - CONF_AUTO_START, DEFAULT_AUTO_START - ), - ) - ] = bool - else: self.homekit_options[CONF_AUTO_START] = self.homekit_options.get( CONF_AUTO_START, DEFAULT_AUTO_START ) - schema_base.update( - { - vol.Optional( - CONF_SAFE_MODE, - default=self.homekit_options.get(CONF_SAFE_MODE, DEFAULT_SAFE_MODE), - ): bool - } - ) + for key in (CONF_DOMAINS, CONF_ENTITIES): + if key in self.homekit_options: + del self.homekit_options[key] + + return self.async_create_entry(title="", data=self.homekit_options) return self.async_show_form( - step_id="advanced", data_schema=vol.Schema(schema_base) + step_id="advanced", + data_schema=vol.Schema( + { + vol.Optional( + CONF_AUTO_START, + default=self.homekit_options.get( + CONF_AUTO_START, DEFAULT_AUTO_START + ), + ): bool + } + ), ) async def async_step_cameras(self, user_input=None): diff --git a/homeassistant/components/homekit/strings.json b/homeassistant/components/homekit/strings.json index 3f12eca0f5f..5ba578f38c3 100644 --- a/homeassistant/components/homekit/strings.json +++ b/homeassistant/components/homekit/strings.json @@ -18,7 +18,7 @@ "mode": "[%key:common::config_flow::data::mode%]", "entities": "Entities" }, - "description": "Choose the entities to be exposed. In accessory mode, only a single entity is exposed. In bridge include mode, all entities in the domain will be exposed unless specific entities are selected. In bridge exclude mode, all entities in the domain will be exposed except for the excluded entities.", + "description": "Choose the entities to be exposed. In accessory mode, only a single entity is exposed. In bridge include mode, all entities in the domain will be exposed unless specific entities are selected. In bridge exclude mode, all entities in the domain will be exposed except for the excluded entities. For best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.", "title": "Select entities to be exposed" }, "cameras": { @@ -30,8 +30,7 @@ }, "advanced": { "data": { - "auto_start": "Autostart (disable if you are calling the homekit.start service manually)", - "safe_mode": "Safe Mode (enable only if pairing fails)" + "auto_start": "Autostart (disable if you are calling the homekit.start service manually)" }, "description": "These settings only need to be adjusted if HomeKit is not functional.", "title": "Advanced Configuration" @@ -44,7 +43,7 @@ "data": { "include_domains": "Domains to include" }, - "description": "The HomeKit integration will allow you to access your Home Assistant entities in HomeKit. In bridge mode, HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML for the primary bridge.", + "description": "The HomeKit integration will allow you to access your Home Assistant entities in HomeKit. In bridge mode, HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML. For best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.", "title": "Activate HomeKit" }, "pairing": { diff --git a/homeassistant/components/homekit/translations/ca.json b/homeassistant/components/homekit/translations/ca.json index 63f461ea344..0870b05a6d1 100644 --- a/homeassistant/components/homekit/translations/ca.json +++ b/homeassistant/components/homekit/translations/ca.json @@ -4,6 +4,20 @@ "port_name_in_use": "Ja hi ha un enlla\u00e7 o accessori configurat amb aquest nom o port." }, "step": { + "accessory_mode": { + "data": { + "entity_id": "Entitat" + }, + "description": "Escull l'entitat que vulguis incloure. En mode accessori, nom\u00e9s s'inclou una sola entitat.", + "title": "Selecciona l'entitat a incloure" + }, + "bridge_mode": { + "data": { + "include_domains": "Dominis a incloure" + }, + "description": "Escull els dominis que vulguis incloure. S'inclouran totes les entitats del domini que siguin compatibles.", + "title": "Selecciona els dominis a incloure" + }, "pairing": { "description": "Tan aviat com {name} estigui llest, la vinculaci\u00f3 estar\u00e0 disponible a \"Notificacions\" com a \"Configuraci\u00f3 de l'enlla\u00e7 HomeKit\".", "title": "Vinculaci\u00f3 HomeKit" @@ -11,9 +25,10 @@ "user": { "data": { "auto_start": "Autoarrencada (desactiva-ho si fas servir Z-Wave o algun altre sistema d'inici lent)", - "include_domains": "Dominis a incloure" + "include_domains": "Dominis a incloure", + "mode": "Mode" }, - "description": "La integraci\u00f3 HomeKit et permet l'acc\u00e9s a les teves entitats de Home Assistant a HomeKit. En mode enlla\u00e7, els enlla\u00e7os HomeKit estan limitats a un m\u00e0xim de 150 accessoris per inst\u00e0ncia (incl\u00f2s el propi enlla\u00e7). Si volguessis enlla\u00e7ar m\u00e9s accessoris que el m\u00e0xim perm\u00e8s, \u00e9s recomanable que utilitzis diferents enlla\u00e7os HomeKit per a dominis diferents. La configuraci\u00f3 avan\u00e7ada d'entitat nom\u00e9s est\u00e0 disponible en YAML per l'enlla\u00e7 prinipal.", + "description": "La integraci\u00f3 HomeKit et permetr\u00e0 l'acc\u00e9s a les teves entitats de Home Assistant a HomeKit. En mode enlla\u00e7, els enlla\u00e7os HomeKit estan limitats a un m\u00e0xim de 150 accessoris per inst\u00e0ncia (incl\u00f2s el propi enlla\u00e7). Si volguessis enlla\u00e7ar m\u00e9s accessoris que el m\u00e0xim perm\u00e8s, \u00e9s recomanable que utilitzis diferents enlla\u00e7os HomeKit per a dominis diferents. La configuraci\u00f3 avan\u00e7ada d'entitat nom\u00e9s est\u00e0 disponible en YAML. Per obtenir el millor rendiment i evitar errors de disponibilitat inesperats , crea i vincula una inst\u00e0ncia HomeKit en mode accessori per a cada repoductor multim\u00e8dia/TV i c\u00e0mera.", "title": "Activaci\u00f3 de HomeKit" } } @@ -22,7 +37,7 @@ "step": { "advanced": { "data": { - "auto_start": "[%key::component::homekit::config::step::user::data::auto_start%]", + "auto_start": "Inici autom\u00e0tic (desactiva-ho si crides el servei homekit.start manualment)", "safe_mode": "Mode segur (habilita-ho nom\u00e9s si falla la vinculaci\u00f3)" }, "description": "Aquests par\u00e0metres nom\u00e9s s'han d'ajustar si HomeKit no \u00e9s funcional.", @@ -40,16 +55,16 @@ "entities": "Entitats", "mode": "Mode" }, - "description": "Tria les entitats que vulguis exposar. En mode accessori, nom\u00e9s s'exposa una sola entitat. En mode enlla\u00e7 inclusiu, s'exposaran totes les entitats del domini tret que se seleccionin algunes en concret. En mode enlla\u00e7 excusiu, s'exposaran totes les entitats del domini excepte les entitats excloses.", - "title": "Selecci\u00f3 de les entitats a exposar" + "description": "Tria les entitats que vulguis incloure. En mode accessori, nom\u00e9s s'inclou una sola entitat. En mode enlla\u00e7 inclusiu, s'exposaran totes les entitats del domini tret de que se'n seleccionin algunes en concret. En mode enlla\u00e7 excusiu, s'inclouran totes les entitats del domini excepte les entitats excloses. Per obtenir el millor rendiment i evitar errors de disponibilitat inesperats , crea i vincula una inst\u00e0ncia HomeKit en mode accessori per a cada repoductor multim\u00e8dia/TV i c\u00e0mera.", + "title": "Selecciona les entitats a incloure" }, "init": { "data": { - "include_domains": "[%key::component::homekit::config::step::user::data::include_domains%]", + "include_domains": "Dominis a incloure", "mode": "Mode" }, - "description": "HomeKit es pot configurar per exposar un enlla\u00e7 o un sol accessori. En mode accessori, nom\u00e9s es pot utilitzar una entitat. El mode accessori \u00e9s necessari en reproductors multim\u00e8dia amb classe de dispositiu TV perqu\u00e8 funcionin correctament. Les entitats a \"Dominis a incloure\" s'exposaran a HomeKit. A la seg\u00fcent pantalla podr\u00e0s seleccionar quines entitats vols incloure o excloure d'aquesta llista.", - "title": "Selecci\u00f3 dels dominis a exposar." + "description": "HomeKit es pot configurar per exposar un enlla\u00e7 o un sol accessori. En mode accessori, nom\u00e9s es pot utilitzar una entitat. El mode accessori \u00e9s necessari perqu\u00e8 els reproductors multim\u00e8dia amb classe de dispositiu TV funcionin correctament. Les entitats a \"Dominis a incloure\" s'inclouran a HomeKit. A la seg\u00fcent pantalla podr\u00e0s seleccionar quines entitats vols incloure o excloure d'aquesta llista.", + "title": "Selecciona els dominis a incloure." }, "yaml": { "description": "Aquesta entrada es controla en YAML", diff --git a/homeassistant/components/homekit/translations/cs.json b/homeassistant/components/homekit/translations/cs.json index c070c852f09..faf1b1d74fc 100644 --- a/homeassistant/components/homekit/translations/cs.json +++ b/homeassistant/components/homekit/translations/cs.json @@ -4,12 +4,18 @@ "port_name_in_use": "P\u0159\u00edslu\u0161enstv\u00ed nebo p\u0159emost\u011bn\u00ed se stejn\u00fdm n\u00e1zvem nebo portem je ji\u017e nastaveno." }, "step": { + "accessory_mode": { + "data": { + "entity_id": "Entita" + } + }, "pairing": { "title": "P\u00e1rov\u00e1n\u00ed s HomeKit" }, "user": { "data": { - "include_domains": "Dom\u00e9ny, kter\u00e9 maj\u00ed b\u00fdt zahrnuty" + "include_domains": "Dom\u00e9ny, kter\u00e9 maj\u00ed b\u00fdt zahrnuty", + "mode": "Re\u017eim" }, "title": "Aktivace HomeKit" } diff --git a/homeassistant/components/homekit/translations/de.json b/homeassistant/components/homekit/translations/de.json index 9b1ec14a1dd..6d69c498bac 100644 --- a/homeassistant/components/homekit/translations/de.json +++ b/homeassistant/components/homekit/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "port_name_in_use": "A bridge with the same name or port is already configured.\nEine HomeKit Bridge mit dem selben Namen oder Port ist bereits vorhanden" + "port_name_in_use": "Eine HomeKit Bridge mit demselben Namen oder Port ist bereits vorhanden." }, "step": { "pairing": { @@ -37,14 +37,15 @@ }, "init": { "data": { + "include_domains": "Einzubeziehende Domains", "mode": "Modus" }, "description": "HomeKit kann so konfiguriert werden, dass eine Br\u00fccke oder ein einzelnes Zubeh\u00f6r verf\u00fcgbar gemacht wird. Im Zubeh\u00f6rmodus kann nur eine einzelne Entit\u00e4t verwendet werden. F\u00fcr Media Player mit der TV-Ger\u00e4teklasse ist ein Zubeh\u00f6rmodus erforderlich, damit sie ordnungsgem\u00e4\u00df funktionieren. Entit\u00e4ten in den \"einzuschlie\u00dfenden Dom\u00e4nen\" werden f\u00fcr HomeKit verf\u00fcgbar gemacht. Auf dem n\u00e4chsten Bildschirm k\u00f6nnen Sie ausw\u00e4hlen, welche Entit\u00e4ten in diese Liste aufgenommen oder aus dieser ausgeschlossen werden sollen.", - "title": "W\u00e4hlen Sie die zu \u00fcberbr\u00fcckenden Dom\u00e4nen aus." + "title": "W\u00e4hle die zu \u00fcberbr\u00fcckenden Dom\u00e4nen aus." }, "yaml": { "description": "Dieser Eintrag wird \u00fcber YAML gesteuert", - "title": "Passen Sie die HomeKit Bridge-Optionen an" + "title": "Passe die HomeKit Bridge-Optionen an" } } } diff --git a/homeassistant/components/homekit/translations/en.json b/homeassistant/components/homekit/translations/en.json index 39aa3522025..c5ffa2e9aa4 100644 --- a/homeassistant/components/homekit/translations/en.json +++ b/homeassistant/components/homekit/translations/en.json @@ -1,19 +1,25 @@ { - "config": { - "abort": { - "port_name_in_use": "An accessory or bridge with the same name or port is already configured." - }, + "options": { "step": { - "pairing": { - "description": "As soon as the {name} is ready, pairing will be available in \u201cNotifications\u201d as \u201cHomeKit Bridge Setup\u201d.", - "title": "Pair HomeKit" + "yaml": { + "title": "Adjust HomeKit Options", + "description": "This entry is controlled via YAML" + }, + "init": { + "data": { + "mode": "[%key:common::config_flow::data::mode%]", + "include_domains": "[%key:component::homekit::config::step::user::data::include_domains%]" + }, + "description": "HomeKit can be configured expose a bridge or a single accessory. In accessory mode, only a single entity can be used. Accessory mode is required for media players with the TV device class to function properly. Entities in the \u201cDomains to include\u201d will be exposed to HomeKit. You will be able to select which entities to include or exclude from this list on the next screen.", + "title": "Select domains to expose." }, "user": { "data": { "auto_start": "Autostart (disable if using Z-Wave or other delayed start system)", - "include_domains": "Domains to include" + "include_domains": "Domains to include", + "mode": "Mode" }, - "description": "The HomeKit integration will allow you to access your Home Assistant entities in HomeKit. In bridge mode, HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML for the primary bridge.", + "description": "The HomeKit integration will allow you to access your Home Assistant entities in HomeKit. In bridge mode, HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML. For best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.", "title": "Activate HomeKit" } } @@ -22,11 +28,11 @@ "step": { "advanced": { "data": { - "auto_start": "Autostart (disable if using Z-Wave or other delayed start system)", + "auto_start": "Autostart (disable if you are calling the homekit.start service manually)", "safe_mode": "Safe Mode (enable only if pairing fails)" }, - "description": "These settings only need to be adjusted if HomeKit is not functional.", - "title": "Advanced Configuration" + "description": "Choose the entities to be exposed. In accessory mode, only a single entity is exposed. In bridge include mode, all entities in the domain will be exposed unless specific entities are selected. In bridge exclude mode, all entities in the domain will be exposed except for the excluded entities. For best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.", + "title": "Select entities to be exposed" }, "cameras": { "data": { @@ -35,26 +41,31 @@ "description": "Check all cameras that support native H.264 streams. If the camera does not output a H.264 stream, the system will transcode the video to H.264 for HomeKit. Transcoding requires a performant CPU and is unlikely to work on single board computers.", "title": "Select camera video codec." }, - "include_exclude": { + "advanced": { "data": { - "entities": "Entities", - "mode": "Mode" + "auto_start": "Autostart (disable if you are calling the homekit.start service manually)" }, - "description": "Choose the entities to be exposed. In accessory mode, only a single entity is exposed. In bridge include mode, all entities in the domain will be exposed unless specific entities are selected. In bridge exclude mode, all entities in the domain will be exposed except for the excluded entities.", - "title": "Select entities to be exposed" - }, - "init": { - "data": { - "include_domains": "Domains to include", - "mode": "Mode" - }, - "description": "HomeKit can be configured expose a bridge or a single accessory. In accessory mode, only a single entity can be used. Accessory mode is required for media players with the TV device class to function properly. Entities in the \u201cDomains to include\u201d will be exposed to HomeKit. You will be able to select which entities to include or exclude from this list on the next screen.", - "title": "Select domains to expose." - }, - "yaml": { - "description": "This entry is controlled via YAML", - "title": "Adjust HomeKit Options" + "description": "These settings only need to be adjusted if HomeKit is not functional.", + "title": "Advanced Configuration" } } + }, + "config": { + "step": { + "user": { + "data": { + "include_domains": "Domains to include" + }, + "description": "The HomeKit integration will allow you to access your Home Assistant entities in HomeKit. In bridge mode, HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML. For best performance, and to prevent unexpected unavailability, create and pair a separate HomeKit instance in accessory mode for each tv media player and camera.", + "title": "Activate HomeKit" + }, + "pairing": { + "title": "Pair HomeKit", + "description": "As soon as the {name} is ready, pairing will be available in \u201cNotifications\u201d as \u201cHomeKit Bridge Setup\u201d." + } + }, + "abort": { + "port_name_in_use": "An accessory or bridge with the same name or port is already configured." + } } -} \ No newline at end of file +} diff --git a/homeassistant/components/homekit/translations/et.json b/homeassistant/components/homekit/translations/et.json index 76a78602bd3..37bff5f9b70 100644 --- a/homeassistant/components/homekit/translations/et.json +++ b/homeassistant/components/homekit/translations/et.json @@ -4,6 +4,20 @@ "port_name_in_use": "Sama nime v\u00f5i pordiga tarvik v\u00f5i sild on juba konfigureeritud." }, "step": { + "accessory_mode": { + "data": { + "entity_id": "Olem" + }, + "description": "Vali kaasatav olem. Lisare\u017eiimis on kaasatav ainult \u00fcks olem.", + "title": "Vali kaasatav olem" + }, + "bridge_mode": { + "data": { + "include_domains": "Kaasatavad domeenid" + }, + "description": "Vali kaasatavad domeenid. Kaasatakse k\u00f5ik domeenis toetatud olemid.", + "title": "Vali kaasatavad domeenid" + }, "pairing": { "description": "Niipea kui {name} on valmis, on sidumine saadaval jaotises \"Notifications\" kui \"HomeKit Bridge Setup\".", "title": "HomeKiti sidumine" @@ -11,9 +25,10 @@ "user": { "data": { "auto_start": "Autostart (keela, kui kasutad Z-Wave'i v\u00f5i muud viivitatud k\u00e4ivituss\u00fcsteemi)", - "include_domains": "Kaasatavad domeenid" + "include_domains": "Kaasatavad domeenid", + "mode": "Re\u017eiim" }, - "description": "HomeKiti integreerimine v\u00f5imaldab teil p\u00e4\u00e4seda juurde HomeKiti \u00fcksustele Home Assistant. Sildire\u017eiimis on HomeKit Bridges piiratud 150 lisaseadmega, sealhulgas sild ise. Kui soovid \u00fchendada rohkem lisatarvikuid, on soovitatav kasutada erinevate domeenide jaoks mitut HomeKiti silda. \u00dcksuse \u00fcksikasjalik konfiguratsioon on esmase silla jaoks saadaval ainult YAML-i kaudu.", + "description": "HomeKiti integreerimine v\u00f5imaldab teil p\u00e4\u00e4seda juurde HomeKiti \u00fcksustele Home Assistant. Sildire\u017eiimis on HomeKit Bridges piiratud 150 lisaseadmega, sealhulgas sild ise. Kui soovid \u00fchendada rohkem lisatarvikuid, on soovitatav kasutada erinevate domeenide jaoks mitut HomeKiti silda. \u00dcksuse \u00fcksikasjalik konfiguratsioon on esmase silla jaoks saadaval ainult YAML-i kaudu. Parema tulemuse saavutamiseks ja ootamatute seadmete kadumise v\u00e4ltimiseks loo ja seo eraldi HomeKiti seade tarviku re\u017eiimis kga meediaesitaja ja kaamera jaoks.", "title": "Aktiveeri HomeKit" } } @@ -22,7 +37,7 @@ "step": { "advanced": { "data": { - "auto_start": "Autostart (keela, kui kasutad Z-Wave'i v\u00f5i muud viivitatud k\u00e4ivituss\u00fcsteemi)", + "auto_start": "Autostart (keela kui kasutad homekit.start teenust k\u00e4sitsi)", "safe_mode": "Turvare\u017eiim (luba ainult siis, kui sidumine nurjub)" }, "description": "Neid s\u00e4tteid tuleb muuta ainult siis kui HomeKit ei t\u00f6\u00f6ta.", @@ -40,8 +55,8 @@ "entities": "Olemid", "mode": "Re\u017eiim" }, - "description": "Vali avaldatavad olemid. Tarvikute re\u017eiimis on avaldatav ainult \u00fcks olem. Silla re\u017eiimis, kuvatakse k\u00f5ik domeeni olemid, v\u00e4lja arvatud juhul, kui valitud on kindlad olemid. Silla v\u00e4listamisre\u017eiimis avaldatakse k\u00f5ik domeeni olemid, v\u00e4lja arvatud v\u00e4listatud olemid.", - "title": "Vali avaldatavad olemid" + "description": "Vali kaasatavad olemid. Tarvikute re\u017eiimis on kaasatav ainult \u00fcks olem. Silla re\u017eiimis, kuvatakse k\u00f5ik domeeni olemid, v\u00e4lja arvatud juhul, kui valitud on kindlad olemid. Silla v\u00e4listamisre\u017eiimis kaasatakse k\u00f5ik domeeni olemid, v\u00e4lja arvatud v\u00e4listatud olemid.", + "title": "Vali kaasatavd olemid" }, "init": { "data": { @@ -49,7 +64,7 @@ "mode": "Re\u017eiim" }, "description": "HomeKiti saab seadistada silla v\u00f5i \u00fche lisaseadme avaldamiseks. Lisare\u017eiimis saab kasutada ainult \u00fchte \u00fcksust. Teleriseadmete klassiga meediumipleierite n\u00f5uetekohaseks toimimiseks on vaja lisare\u017eiimi. \u201eKaasatavate domeenide\u201d \u00fcksused puutuvad kokku HomeKitiga. J\u00e4rgmisel ekraanil saad valida, millised \u00fcksused sellesse loendisse lisada v\u00f5i sellest v\u00e4lja j\u00e4tta.", - "title": "Valige avaldatavad domeenid." + "title": "Vali kaasatavad domeenid" }, "yaml": { "description": "Seda sisestust juhitakse YAML-i kaudu", diff --git a/homeassistant/components/homekit/translations/it.json b/homeassistant/components/homekit/translations/it.json index 7e9c8a05b9d..9a85d1e6e9f 100644 --- a/homeassistant/components/homekit/translations/it.json +++ b/homeassistant/components/homekit/translations/it.json @@ -4,6 +4,20 @@ "port_name_in_use": "Un accessorio o un bridge con lo stesso nome o porta \u00e8 gi\u00e0 configurato." }, "step": { + "accessory_mode": { + "data": { + "entity_id": "Entit\u00e0" + }, + "description": "Scegli l'entit\u00e0 da includere. In modalit\u00e0 accessorio, \u00e8 inclusa solo una singola entit\u00e0.", + "title": "Seleziona l'entit\u00e0 da includere" + }, + "bridge_mode": { + "data": { + "include_domains": "Domini da includere" + }, + "description": "Scegli i domini da includere. Verranno incluse tutte le entit\u00e0 supportate nel dominio.", + "title": "Seleziona i domini da includere" + }, "pairing": { "description": "Non appena il {name} \u00e8 pronto, l'associazione sar\u00e0 disponibile in \"Notifiche\" come \"Configurazione HomeKit Bridge\".", "title": "Associa HomeKit" @@ -11,7 +25,8 @@ "user": { "data": { "auto_start": "Avvio automatico (disabilitare se si utilizza Z-Wave o un altro sistema di avvio ritardato)", - "include_domains": "Domini da includere" + "include_domains": "Domini da includere", + "mode": "Modalit\u00e0" }, "description": "L'integrazione di HomeKit ti consentir\u00e0 di accedere alle entit\u00e0 di Home Assistant in HomeKit. In modalit\u00e0 bridge, i bridge HomeKit sono limitati a 150 accessori per istanza, incluso il bridge stesso. Se desideri eseguire il bridge di un numero di accessori superiore a quello massimo, si consiglia di utilizzare pi\u00f9 bridge HomeKit per domini diversi. La configurazione dettagliata dell'entit\u00e0 \u00e8 disponibile solo tramite YAML per il bridge principale.", "title": "Attiva HomeKit" @@ -22,7 +37,7 @@ "step": { "advanced": { "data": { - "auto_start": "Avvio automatico (disabilitare se si utilizza Z-Wave o un altro sistema di avvio ritardato)", + "auto_start": "Avvio automatico (disabilitare se stai chiamando manualmente il servizio homekit.start)", "safe_mode": "Modalit\u00e0 provvisoria (attivare solo in caso di errore di associazione)" }, "description": "Queste impostazioni devono essere regolate solo se HomeKit non funziona.", @@ -40,8 +55,8 @@ "entities": "Entit\u00e0", "mode": "Modalit\u00e0" }, - "description": "Scegliere le entit\u00e0 da esporre. In modalit\u00e0 accessorio, \u00e8 esposta una sola entit\u00e0. In modalit\u00e0 di inclusione bridge, tutte le entit\u00e0 nel dominio saranno esposte, a meno che non siano selezionate entit\u00e0 specifiche. In modalit\u00e0 di esclusione bridge, tutte le entit\u00e0 nel dominio saranno esposte, ad eccezione delle entit\u00e0 escluse.", - "title": "Selezionare le entit\u00e0 da esporre" + "description": "Scegliere le entit\u00e0 da includere. In modalit\u00e0 accessorio, \u00e8 inclusa una sola entit\u00e0. In modalit\u00e0 di inclusione bridge, tutte le entit\u00e0 nel dominio saranno incluse, a meno che non siano selezionate entit\u00e0 specifiche. In modalit\u00e0 di esclusione bridge, tutte le entit\u00e0 nel dominio saranno incluse, ad eccezione delle entit\u00e0 escluse. Per prestazioni ottimali e per evitare una indisponibilit\u00e0 imprevista, creare e associare un'istanza HomeKit separata in modalit\u00e0 accessorio per ogni lettore multimediale, TV e videocamera.", + "title": "Seleziona le entit\u00e0 da includere" }, "init": { "data": { @@ -49,7 +64,7 @@ "mode": "Modalit\u00e0" }, "description": "HomeKit pu\u00f2 essere configurato esponendo un bridge o un singolo accessorio. In modalit\u00e0 accessorio, pu\u00f2 essere utilizzata solo una singola entit\u00e0. La modalit\u00e0 accessorio \u00e8 necessaria per il corretto funzionamento dei lettori multimediali con la classe di apparecchi TV. Le entit\u00e0 nei \"Domini da includere\" saranno esposte ad HomeKit. Sar\u00e0 possibile selezionare quali entit\u00e0 includere o escludere da questo elenco nella schermata successiva.", - "title": "Selezionare i domini da esporre." + "title": "Seleziona i domini da includere." }, "yaml": { "description": "Questa voce \u00e8 controllata tramite YAML", diff --git a/homeassistant/components/homekit/translations/no.json b/homeassistant/components/homekit/translations/no.json index 7eff4d37668..9a64def4156 100644 --- a/homeassistant/components/homekit/translations/no.json +++ b/homeassistant/components/homekit/translations/no.json @@ -4,6 +4,20 @@ "port_name_in_use": "Et tilbeh\u00f8r eller bro med samme navn eller port er allerede konfigurert." }, "step": { + "accessory_mode": { + "data": { + "entity_id": "Enhet" + }, + "description": "Velg enheten som skal inkluderes. I tilbeh\u00f8rsmodus er bare en enkelt enhet inkludert.", + "title": "Velg enhet som skal inkluderes" + }, + "bridge_mode": { + "data": { + "include_domains": "Domener \u00e5 inkludere" + }, + "description": "Velg domenene som skal inkluderes. Alle st\u00f8ttede enheter i domenet vil bli inkludert.", + "title": "Velg domener som skal inkluderes" + }, "pairing": { "description": "S\u00e5 snart {name} er klart, vil sammenkobling v\u00e6re tilgjengelig i \"Notifications\" som \"HomeKit Bridge Setup\".", "title": "Koble sammen HomeKit" @@ -11,9 +25,10 @@ "user": { "data": { "auto_start": "Autostart (deaktiver hvis du bruker Z-Wave eller annet forsinket startsystem)", - "include_domains": "Domener \u00e5 inkludere" + "include_domains": "Domener \u00e5 inkludere", + "mode": "Modus" }, - "description": "HomeKit-integrasjonen gir deg tilgang til Home Assistant-entitetene dine i HomeKit. I bromodus er HomeKit Broer begrenset til 150 tilbeh\u00f8rsenhet per forekomst inkludert selve broen. Hvis du \u00f8nsker \u00e5 \u00f8ke maksimalt antall tilbeh\u00f8rsenheter, anbefales det at du bruker flere HomeKit-broer for forskjellige domener. Detaljert entitetskonfigurasjon er bare tilgjengelig via YAML for den prim\u00e6re broen.", + "description": "HomeKit-integrasjonen gir deg tilgang til Home Assistant-enhetene dine i HomeKit. I bromodus er HomeKit Bridges begrenset til 150 tilbeh\u00f8r per forekomst inkludert selve broen. Hvis du \u00f8nsker \u00e5 bygge bro over maksimalt antall tilbeh\u00f8r, anbefales det at du bruker flere HomeKit-broer for forskjellige domener. Detaljert enhetskonfigurasjon er bare tilgjengelig via YAML. For best ytelse og for \u00e5 forhindre uventet utilgjengelighet, opprett og par sammen en egen HomeKit-forekomst i tilbeh\u00f8rsmodus for hver tv-mediaspiller og kamera.", "title": "Aktiver HomeKit" } } @@ -22,7 +37,7 @@ "step": { "advanced": { "data": { - "auto_start": "Autostart (deaktiver hvis du bruker Z-Wave eller annet forsinket startsystem)", + "auto_start": "Autostart (deaktiver hvis du ringer til homekit.start-tjenesten manuelt)", "safe_mode": "Sikker modus (aktiver bare hvis sammenkoblingen mislykkes)" }, "description": "Disse innstillingene m\u00e5 bare justeres hvis HomeKit ikke fungerer.", @@ -40,16 +55,16 @@ "entities": "Entiteter", "mode": "Modus" }, - "description": "Velg entitene som skal eksponeres. I tilbeh\u00f8rsmodus er bare en enkelt entitet eksponert. I bro-inkluderingsmodus vil alle entiteter i domenet bli eksponert med mindre spesifikke entiteter er valgt. I bro-ekskluderingsmodus vil alle entiteter i domenet bli eksponert bortsett fra de ekskluderte entitetene.", - "title": "Velg entiteter som skal eksponeres" + "description": "Velg enhetene som skal inkluderes. I tilbeh\u00f8rsmodus er bare \u00e9n enkelt enhet inkludert. I bridge include-modus inkluderes alle enheter i domenet med mindre bestemte enheter er valgt. I brounnlatingsmodus inkluderes alle enheter i domenet, med unntak av de utelatte enhetene. For best mulig ytelse, og for \u00e5 forhindre uventet utilgjengelighet, opprett og par en separat HomeKit-forekomst i tilbeh\u00f8rsmodus for hver tv-mediespiller og kamera.", + "title": "Velg enheter som skal inkluderes" }, "init": { "data": { "include_domains": "Domener \u00e5 inkludere", "mode": "Modus" }, - "description": "HomeKit kan konfigureres for \u00e5 eksponere en bro eller en enkelt tilbeh\u00f8rsenhet. I tilbeh\u00f8rsmodus kan bare en enkelt entitet brukes. Tilbeh\u00f8rsmodus er n\u00f8dvendig for at mediaspillere med TV-enhetsklasse skal fungere skikkelig. Entiteter i \u201cDomains to include\u201d vil bli eksponert for HomeKit. Du vil kunne velge hvilke entiteter du vil inkludere eller ekskludere fra denne listen p\u00e5 neste skjermbilde.", - "title": "Velg domener du vil eksponere." + "description": "HomeKit kan konfigureres vise en bro eller ett enkelt tilbeh\u00f8r. I tilbeh\u00f8rsmodus kan bare \u00e9n enkelt enhet brukes. Tilbeh\u00f8rsmodus kreves for at mediespillere med TV-enhetsklassen skal fungere som de skal. Enheter i \"Domener som skal inkluderes\" inkluderes i HomeKit. Du kan velge hvilke enheter som skal inkluderes eller ekskluderes fra denne listen p\u00e5 neste skjermbilde.", + "title": "Velg domener som skal inkluderes." }, "yaml": { "description": "Denne oppf\u00f8ringen kontrolleres via YAML", diff --git a/homeassistant/components/homekit/translations/pl.json b/homeassistant/components/homekit/translations/pl.json index 3210f0f4430..2679a4de20a 100644 --- a/homeassistant/components/homekit/translations/pl.json +++ b/homeassistant/components/homekit/translations/pl.json @@ -4,6 +4,20 @@ "port_name_in_use": "Akcesorium lub mostek o tej samej nazwie lub adresie IP jest ju\u017c skonfigurowany" }, "step": { + "accessory_mode": { + "data": { + "entity_id": "Encja" + }, + "description": "Wybierz uwzgl\u0119dniane encje. W trybie akcesori\u00f3w uwzgl\u0119dniana jest tylko jedna encja.", + "title": "Wybierz uwzgl\u0119dniane encje" + }, + "bridge_mode": { + "data": { + "include_domains": "Domeny do uwzgl\u0119dnienia" + }, + "description": "Wybierz uwzgl\u0119dniane domeny. Wszystkie obs\u0142ugiwane encje w domenie zostan\u0105 uwzgl\u0119dnione.", + "title": "Wybierz uwzgl\u0119dniane domeny" + }, "pairing": { "description": "Gdy tylko {name} b\u0119dzie gotowy, opcja parowania b\u0119dzie dost\u0119pna w \u201ePowiadomieniach\u201d jako \u201eKonfiguracja mostka HomeKit\u201d.", "title": "Parowanie z HomeKit" @@ -11,9 +25,10 @@ "user": { "data": { "auto_start": "Automatyczne uruchomienie (wy\u0142\u0105cz, je\u015bli u\u017cywasz Z-Wave lub innej integracji op\u00f3\u017aniaj\u0105cej start systemu)", - "include_domains": "Domeny do uwzgl\u0119dnienia" + "include_domains": "Domeny do uwzgl\u0119dnienia", + "mode": "Tryb" }, - "description": "Integracja HomeKit pozwala na dost\u0119p do Twoich encji Home Assistant w HomeKit. W trybie \"Mostka\", mostki HomeKit s\u0105 ograniczone do 150 urz\u0105dze\u0144, w\u0142\u0105czaj\u0105c w to sam mostek. Je\u015bli chcesz wi\u0119cej ni\u017c dozwolona maksymalna liczba urz\u0105dze\u0144, zaleca si\u0119 u\u017cywanie wielu most\u00f3w HomeKit dla r\u00f3\u017cnych domen. Szczeg\u00f3\u0142owa konfiguracja encji jest dost\u0119pna tylko w trybie YAML dla g\u0142\u00f3wnego mostka.", + "description": "Integracja HomeKit pozwala na dost\u0119p do Twoich encji Home Assistant w HomeKit. W trybie \"Mostka\", mostki HomeKit s\u0105 ograniczone do 150 urz\u0105dze\u0144, w\u0142\u0105czaj\u0105c w to sam mostek. Je\u015bli chcesz wi\u0119cej ni\u017c dozwolona maksymalna liczba urz\u0105dze\u0144, zaleca si\u0119 u\u017cywanie wielu most\u00f3w HomeKit dla r\u00f3\u017cnych domen. Szczeg\u00f3\u0142owa konfiguracja encji jest dost\u0119pna tylko w trybie YAML dla g\u0142\u00f3wnego mostka. Dla najlepszej wydajno\u015bci oraz by zapobiec nieprzewidzianej niedost\u0119pno\u015bci urz\u0105dzenia, utw\u00f3rz i sparuj oddzieln\u0105 instancj\u0119 HomeKit w trybie akcesorium dla ka\u017cdego media playera oraz kamery.", "title": "Aktywacja HomeKit" } } @@ -22,7 +37,7 @@ "step": { "advanced": { "data": { - "auto_start": "Automatyczne uruchomienie (wy\u0142\u0105cz, je\u015bli u\u017cywasz Z-Wave lub innej integracji op\u00f3\u017aniaj\u0105cej start systemu)", + "auto_start": "Automatyczne uruchomienie (wy\u0142\u0105cz, je\u015bli r\u0119cznie uruchamiasz us\u0142ug\u0119 homekit.start)", "safe_mode": "Tryb awaryjny (w\u0142\u0105cz tylko wtedy, gdy parowanie nie powiedzie si\u0119)" }, "description": "Te ustawienia nale\u017cy dostosowa\u0107 tylko wtedy, gdy HomeKit nie dzia\u0142a.", @@ -40,8 +55,8 @@ "entities": "Encje", "mode": "Tryb" }, - "description": "Wybierz encje, kt\u00f3re maj\u0105 by\u0107 widoczne. W trybie \"Akcesorium\", tylko jedna encja jest widoczna. W trybie \"Uwzgl\u0119dnij mostek\", wszystkie encje w danej domenie b\u0119d\u0105 widoczne, chyba \u017ce wybrane s\u0105 tylko konkretne encje. W trybie \"Wyklucz mostek\", wszystkie encje b\u0119d\u0105 widoczne, z wyj\u0105tkiem tych wybranych.", - "title": "Wybierz encje, kt\u00f3re maj\u0105 by\u0107 widoczne" + "description": "Wybierz encje, kt\u00f3re maj\u0105 by\u0107 uwzgl\u0119dnione. W trybie \"Akcesorium\" tylko jedna encja jest uwzgl\u0119dniona. W trybie \"Uwzgl\u0119dnij mostek\", wszystkie encje w danej domenie b\u0119d\u0105 uwzgl\u0119dnione, chyba \u017ce wybrane s\u0105 tylko konkretne encje. W trybie \"Wyklucz mostek\", wszystkie encje b\u0119d\u0105 uwzgl\u0119dnione, z wyj\u0105tkiem tych wybranych. Dla najlepszej wydajno\u015bci oraz by zapobiec nieprzewidzianej niedost\u0119pno\u015bci urz\u0105dzenia, utw\u00f3rz i sparuj oddzieln\u0105 instancj\u0119 HomeKit w trybie akcesorium dla ka\u017cdego media playera oraz kamery.", + "title": "Wybierz encje, kt\u00f3re maj\u0105 by\u0107 uwzgl\u0119dnione" }, "init": { "data": { diff --git a/homeassistant/components/homekit/translations/ro.json b/homeassistant/components/homekit/translations/ro.json new file mode 100644 index 00000000000..82e8344417b --- /dev/null +++ b/homeassistant/components/homekit/translations/ro.json @@ -0,0 +1,9 @@ +{ + "config": { + "step": { + "user": { + "description": "Integrarea HomeKit v\u0103 va permite s\u0103 accesa\u021bi entit\u0103\u021bile Home Assistant din HomeKit. \u00cen modul bridge, HomeKit Bridges sunt limitate la 150 de accesorii pe instan\u021b\u0103, inclusiv bridge-ul \u00een sine. Dac\u0103 dori\u021bi s\u0103 face\u021bi mai mult dec\u00e2t num\u0103rul maxim de accesorii, este recomandat s\u0103 utiliza\u021bi mai multe poduri HomeKit pentru diferite domenii. Configurarea detaliat\u0103 a entit\u0103\u021bii este disponibil\u0103 numai prin YAML. Pentru cele mai bune performan\u021be \u0219i pentru a preveni indisponibilitatea nea\u0219teptat\u0103, crea\u021bi \u0219i \u00eemperechea\u021bi o instan\u021b\u0103 HomeKit separat\u0103 \u00een modul accesoriu pentru fiecare player media TV \u0219i camer\u0103." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/ru.json b/homeassistant/components/homekit/translations/ru.json index 3cb5e84936a..6cf96c2dd78 100644 --- a/homeassistant/components/homekit/translations/ru.json +++ b/homeassistant/components/homekit/translations/ru.json @@ -4,6 +4,11 @@ "port_name_in_use": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c \u0436\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c \u0438\u043b\u0438 \u043f\u043e\u0440\u0442\u043e\u043c \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "step": { + "accessory_mode": { + "data": { + "entity_id": "\u041e\u0431\u044a\u0435\u043a\u0442" + } + }, "pairing": { "description": "\u041a\u0430\u043a \u0442\u043e\u043b\u044c\u043a\u043e {name} \u0431\u0443\u0434\u0435\u0442 \u0433\u043e\u0442\u043e\u0432\u043e, \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0431\u0443\u0434\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e \u0432 \"\u0423\u0432\u0435\u0434\u043e\u043c\u043b\u0435\u043d\u0438\u044f\u0445\" \u043a\u0430\u043a \"HomeKit Bridge Setup\".", "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 HomeKit" @@ -13,7 +18,7 @@ "auto_start": "\u0410\u0432\u0442\u043e\u0437\u0430\u043f\u0443\u0441\u043a (\u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 Z-Wave \u0438\u043b\u0438 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u044b \u043e\u0442\u043b\u043e\u0436\u0435\u043d\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0443\u0441\u043a\u0430)", "include_domains": "\u0412\u044b\u0431\u0440\u0430\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u044b" }, - "description": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u043e\u0431\u044a\u0435\u043a\u0442\u0430\u043c Home Assistant \u0447\u0435\u0440\u0435\u0437 HomeKit. HomeKit Bridge \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d 150 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430\u043c\u0438 \u043d\u0430 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440, \u0432\u043a\u043b\u044e\u0447\u0430\u044f \u0441\u0430\u043c \u0431\u0440\u0438\u0434\u0436. \u0415\u0441\u043b\u0438 \u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0431\u043e\u043b\u044c\u0448\u0435, \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e HomeKit Bridge \u0434\u043b\u044f \u0440\u0430\u0437\u043d\u044b\u0445 \u0434\u043e\u043c\u0435\u043d\u043e\u0432. \u0414\u0435\u0442\u0430\u043b\u044c\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 YAML \u0434\u043b\u044f \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0433\u043e \u0431\u0440\u0438\u0434\u0436\u0430.", + "description": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u043f\u043e\u0437\u0432\u043e\u043b\u044f\u0435\u0442 \u043f\u043e\u043b\u0443\u0447\u0430\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u043e\u0431\u044a\u0435\u043a\u0442\u0430\u043c Home Assistant \u0447\u0435\u0440\u0435\u0437 HomeKit. HomeKit Bridge \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d 150 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430\u043c\u0438 \u043d\u0430 \u044d\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440, \u0432\u043a\u043b\u044e\u0447\u0430\u044f \u0441\u0430\u043c \u0431\u0440\u0438\u0434\u0436. \u0415\u0441\u043b\u0438 \u0412\u0430\u043c \u043d\u0443\u0436\u043d\u043e \u0431\u043e\u043b\u044c\u0448\u0435, \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e HomeKit Bridge \u0434\u043b\u044f \u0440\u0430\u0437\u043d\u044b\u0445 \u0434\u043e\u043c\u0435\u043d\u043e\u0432. \u0414\u0435\u0442\u0430\u043b\u044c\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043e\u0431\u044a\u0435\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0442\u043e\u043b\u044c\u043a\u043e \u0447\u0435\u0440\u0435\u0437 YAML. \u0414\u043b\u044f \u043b\u0443\u0447\u0448\u0435\u0439 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u0438 \u043f\u0440\u0435\u0434\u043e\u0442\u0432\u0440\u0430\u0449\u0435\u043d\u0438\u044f \u043d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u043e\u0441\u0442\u0435\u0439 \u0441\u043e\u0437\u0434\u0430\u0439\u0442\u0435 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u0443\u044e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043c\u0435\u0434\u0438\u0430\u043f\u043b\u0435\u0435\u0440\u0430 \u0438\u043b\u0438 \u043a\u0430\u043c\u0435\u0440\u044b.", "title": "HomeKit" } } @@ -22,7 +27,7 @@ "step": { "advanced": { "data": { - "auto_start": "\u0410\u0432\u0442\u043e\u0437\u0430\u043f\u0443\u0441\u043a (\u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0438 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u0438 Z-Wave \u0438\u043b\u0438 \u0434\u0440\u0443\u0433\u043e\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u044b \u043e\u0442\u043b\u043e\u0436\u0435\u043d\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0443\u0441\u043a\u0430)", + "auto_start": "\u0410\u0432\u0442\u043e\u0437\u0430\u043f\u0443\u0441\u043a (\u043e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u0435, \u0435\u0441\u043b\u0438 \u0412\u044b \u0432\u0440\u0443\u0447\u043d\u0443\u044e \u0432\u044b\u0437\u044b\u0432\u0430\u0435\u0442\u0435 \u0441\u043b\u0443\u0436\u0431\u0443 homekit.start)", "safe_mode": "\u0411\u0435\u0437\u043e\u043f\u0430\u0441\u043d\u044b\u0439 \u0440\u0435\u0436\u0438\u043c (\u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0435 \u0442\u043e\u043b\u044c\u043a\u043e \u0432 \u0441\u043b\u0443\u0447\u0430\u0435 \u0441\u0431\u043e\u044f \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f)" }, "description": "\u042d\u0442\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b, \u0442\u043e\u043b\u044c\u043a\u043e \u0435\u0441\u043b\u0438 HomeKit \u043d\u0435 \u0440\u0430\u0431\u043e\u0442\u0430\u0435\u0442.", @@ -40,7 +45,7 @@ "entities": "\u041e\u0431\u044a\u0435\u043a\u0442\u044b", "mode": "\u0420\u0435\u0436\u0438\u043c" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u043c\u043e\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u043e\u0431\u044a\u0435\u043a\u0442. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u043c\u043e\u0441\u0442\u0430 \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0449\u0438\u0435 \u0434\u043e\u043c\u0435\u043d\u0443, \u0435\u0441\u043b\u0438 \u043d\u0435 \u0432\u044b\u0431\u0440\u0430\u043d\u044b \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0449\u0438\u0435 \u0434\u043e\u043c\u0435\u043d\u0443, \u043a\u0440\u043e\u043c\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044b\u0445.", + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u043c\u043e\u0436\u043d\u043e \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d \u043e\u0431\u044a\u0435\u043a\u0442. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u043c\u043e\u0441\u0442\u0430 \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0449\u0438\u0435 \u0434\u043e\u043c\u0435\u043d\u0443, \u0435\u0441\u043b\u0438 \u043d\u0435 \u0432\u044b\u0431\u0440\u0430\u043d\u044b \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b. \u0412 \u0440\u0435\u0436\u0438\u043c\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0431\u0443\u0434\u0443\u0442 \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u044b \u0432\u0441\u0435 \u043e\u0431\u044a\u0435\u043a\u0442\u044b, \u043f\u0440\u0438\u043d\u0430\u0434\u043b\u0435\u0436\u0430\u0449\u0438\u0435 \u0434\u043e\u043c\u0435\u043d\u0443, \u043a\u0440\u043e\u043c\u0435 \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044b\u0445. \u0414\u043b\u044f \u043b\u0443\u0447\u0448\u0435\u0439 \u043f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u0438 \u043f\u0440\u0435\u0434\u043e\u0442\u0432\u0440\u0430\u0449\u0435\u043d\u0438\u044f \u043d\u0435\u0438\u0441\u043f\u0440\u0430\u0432\u043d\u043e\u0441\u0442\u0435\u0439 \u0441\u043e\u0437\u0434\u0430\u0439\u0442\u0435 \u043e\u0442\u0434\u0435\u043b\u044c\u043d\u0443\u044e \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044e \u0432 \u0440\u0435\u0436\u0438\u043c\u0435 \u0430\u043a\u0441\u0435\u0441\u0441\u0443\u0430\u0440\u0430 \u0434\u043b\u044f \u043a\u0430\u0436\u0434\u043e\u0433\u043e \u043c\u0435\u0434\u0438\u0430\u043f\u043b\u0435\u0435\u0440\u0430 \u0438\u043b\u0438 \u043a\u0430\u043c\u0435\u0440\u044b.", "title": "\u0412\u044b\u0431\u043e\u0440 \u043e\u0431\u044a\u0435\u043a\u0442\u043e\u0432 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0438 \u0432 HomeKit" }, "init": { diff --git a/homeassistant/components/homekit/translations/sv.json b/homeassistant/components/homekit/translations/sv.json index 5aa9507a85d..1e2fcae04b5 100644 --- a/homeassistant/components/homekit/translations/sv.json +++ b/homeassistant/components/homekit/translations/sv.json @@ -1,4 +1,19 @@ { + "config": { + "step": { + "bridge_mode": { + "data": { + "include_domains": "Dom\u00e4ner att inkludera" + } + }, + "pairing": { + "title": "Para HomeKit" + }, + "user": { + "title": "Aktivera HomeKit" + } + } + }, "options": { "step": { "cameras": { @@ -7,6 +22,12 @@ }, "description": "Kontrollera alla kameror som st\u00f6der inbyggda H.264-str\u00f6mmar. Om kameran inte skickar ut en H.264-str\u00f6m kodar systemet videon till H.264 f\u00f6r HomeKit. Transkodning kr\u00e4ver h\u00f6g prestanda och kommer troligtvis inte att fungera p\u00e5 enkortsdatorer.", "title": "V\u00e4lj kamerans videoavkodare." + }, + "init": { + "data": { + "include_domains": "Dom\u00e4ner att inkludera" + }, + "title": "V\u00e4lj dom\u00e4ner som ska inkluderas." } } } diff --git a/homeassistant/components/homekit/translations/tr.json b/homeassistant/components/homekit/translations/tr.json new file mode 100644 index 00000000000..f9391fd0686 --- /dev/null +++ b/homeassistant/components/homekit/translations/tr.json @@ -0,0 +1,60 @@ +{ + "config": { + "abort": { + "port_name_in_use": "Ayn\u0131 ada veya ba\u011flant\u0131 noktas\u0131na sahip bir aksesuar veya k\u00f6pr\u00fc zaten yap\u0131land\u0131r\u0131lm\u0131\u015f." + }, + "step": { + "accessory_mode": { + "data": { + "entity_id": "Varl\u0131k" + }, + "description": "Dahil edilecek varl\u0131\u011f\u0131 se\u00e7in. Aksesuar modunda, yaln\u0131zca tek bir varl\u0131k dahildir.", + "title": "Dahil edilecek varl\u0131\u011f\u0131 se\u00e7in" + }, + "bridge_mode": { + "data": { + "include_domains": "\u0130\u00e7erecek etki alanlar\u0131" + }, + "description": "Dahil edilecek alanlar\u0131 se\u00e7in. Etki alan\u0131ndaki t\u00fcm desteklenen varl\u0131klar dahil edilecektir.", + "title": "Dahil edilecek etki alanlar\u0131n\u0131 se\u00e7in" + }, + "pairing": { + "description": "{name} haz\u0131r olur olmaz e\u015fle\u015ftirme, \"Bildirimler\" i\u00e7inde \"HomeKit K\u00f6pr\u00fc Kurulumu\" olarak mevcut olacakt\u0131r.", + "title": "HomeKit'i E\u015fle\u015ftir" + }, + "user": { + "data": { + "mode": "Mod" + } + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "safe_mode": "G\u00fcvenli Mod (yaln\u0131zca e\u015fle\u015ftirme ba\u015far\u0131s\u0131z olursa etkinle\u015ftirin)" + } + }, + "cameras": { + "data": { + "camera_copy": "Yerel H.264 ak\u0131\u015flar\u0131n\u0131 destekleyen kameralar" + }, + "description": "Yerel H.264 ak\u0131\u015flar\u0131n\u0131 destekleyen t\u00fcm kameralar\u0131 kontrol edin. Kamera bir H.264 ak\u0131\u015f\u0131 vermezse, sistem videoyu HomeKit i\u00e7in H.264'e d\u00f6n\u00fc\u015ft\u00fcr\u00fcr. Kod d\u00f6n\u00fc\u015ft\u00fcrme, y\u00fcksek performansl\u0131 bir CPU gerektirir ve tek kartl\u0131 bilgisayarlarda \u00e7al\u0131\u015fma olas\u0131l\u0131\u011f\u0131 d\u00fc\u015f\u00fckt\u00fcr.", + "title": "Kamera video codec bile\u015fenini se\u00e7in." + }, + "include_exclude": { + "data": { + "entities": "Varl\u0131klar", + "mode": "Mod" + }, + "title": "Dahil edilecek varl\u0131klar\u0131 se\u00e7in" + }, + "init": { + "data": { + "mode": "Mod" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/uk.json b/homeassistant/components/homekit/translations/uk.json index 10cd42ccecb..876b200bdf8 100644 --- a/homeassistant/components/homekit/translations/uk.json +++ b/homeassistant/components/homekit/translations/uk.json @@ -1,10 +1,59 @@ { + "config": { + "abort": { + "port_name_in_use": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0437 \u0442\u0430\u043a\u043e\u044e \u0436 \u043d\u0430\u0437\u0432\u043e\u044e \u0430\u0431\u043e \u043f\u043e\u0440\u0442\u043e\u043c \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435." + }, + "step": { + "pairing": { + "description": "\u042f\u043a \u0442\u0456\u043b\u044c\u043a\u0438 {name} \u0431\u0443\u0434\u0435 \u0433\u043e\u0442\u043e\u0432\u0438\u0439, \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438 \u0431\u0443\u0434\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e \u0432 \"\u0421\u043f\u043e\u0432\u0456\u0449\u0435\u043d\u043d\u044f\u0445\" \u044f\u043a \"HomeKit Bridge Setup\".", + "title": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f \u0437 HomeKit" + }, + "user": { + "data": { + "auto_start": "\u0410\u0432\u0442\u043e\u0437\u0430\u043f\u0443\u0441\u043a (\u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u043f\u0440\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u0456 Z-Wave \u0430\u0431\u043e \u0456\u043d\u0448\u043e\u0457 \u0441\u0438\u0441\u0442\u0435\u043c\u0438 \u0432\u0456\u0434\u043a\u043b\u0430\u0434\u0435\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0443\u0441\u043a\u0443)", + "include_domains": "\u0412\u0438\u0431\u0440\u0430\u0442\u0438 \u0434\u043e\u043c\u0435\u043d\u0438" + }, + "description": "\u0426\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f \u0434\u043e\u0437\u0432\u043e\u043b\u044f\u0454 \u043e\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u043e\u0431'\u0454\u043a\u0442\u0456\u0432 Home Assistant \u0447\u0435\u0440\u0435\u0437 HomeKit. HomeKit Bridge \u043e\u0431\u043c\u0435\u0436\u0435\u043d\u0438\u0439 150 \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440\u0430\u043c\u0438 \u043d\u0430 \u0435\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440, \u0432\u043a\u043b\u044e\u0447\u0430\u044e\u0447\u0438 \u0441\u0430\u043c \u0431\u0440\u0438\u0434\u0436. \u042f\u043a\u0449\u043e \u0412\u0430\u043c \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0431\u0456\u043b\u044c\u0448\u0435, \u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0454\u0442\u044c\u0441\u044f \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u043a\u0456\u043b\u044c\u043a\u0430 HomeKit Bridge \u0434\u043b\u044f \u0440\u0456\u0437\u043d\u0438\u0445 \u0434\u043e\u043c\u0435\u043d\u0456\u0432. \u0414\u0435\u0442\u0430\u043b\u044c\u043d\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u043a\u043e\u0436\u043d\u043e\u0433\u043e \u043e\u0431'\u0454\u043a\u0442\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0435 \u0442\u0456\u043b\u044c\u043a\u0438 \u0447\u0435\u0440\u0435\u0437 YAML \u0434\u043b\u044f \u043e\u0441\u043d\u043e\u0432\u043d\u043e\u0433\u043e \u043c\u043e\u0441\u0442\u0430.", + "title": "HomeKit" + } + } + }, "options": { "step": { + "advanced": { + "data": { + "auto_start": "\u0410\u0432\u0442\u043e\u0437\u0430\u043f\u0443\u0441\u043a (\u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u043f\u0440\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u0456 Z-Wave \u0430\u0431\u043e \u0456\u043d\u0448\u043e\u0457 \u0441\u0438\u0441\u0442\u0435\u043c\u0438 \u0432\u0456\u0434\u043a\u043b\u0430\u0434\u0435\u043d\u043e\u0433\u043e \u0437\u0430\u043f\u0443\u0441\u043a\u0443)", + "safe_mode": "\u0411\u0435\u0437\u043f\u0435\u0447\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c (\u0443\u0432\u0456\u043c\u043a\u043d\u0456\u0442\u044c \u0442\u0456\u043b\u044c\u043a\u0438 \u0432 \u0440\u0430\u0437\u0456 \u0437\u0431\u043e\u044e \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f)" + }, + "description": "\u0426\u0456 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u0456, \u043b\u0438\u0448\u0435 \u044f\u043a\u0449\u043e HomeKit \u043d\u0435 \u043f\u0440\u0430\u0446\u044e\u0454.", + "title": "\u0420\u043e\u0437\u0448\u0438\u0440\u0435\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f" + }, + "cameras": { + "data": { + "camera_copy": "\u041a\u0430\u043c\u0435\u0440\u0438, \u044f\u043a\u0456 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u044e\u0442\u044c \u043f\u043e\u0442\u043e\u043a\u0438 H.264" + }, + "description": "\u042f\u043a\u0449\u043e \u043a\u0430\u043c\u0435\u0440\u0430 \u043d\u0435 \u0432\u0438\u0432\u043e\u0434\u0438\u0442\u044c \u043f\u043e\u0442\u0456\u043a H.264, \u0441\u0438\u0441\u0442\u0435\u043c\u0430 \u043f\u0435\u0440\u0435\u043a\u043e\u0434\u043e\u0432\u0443\u0454 \u0432\u0456\u0434\u0435\u043e \u0432 H.264 \u0434\u043b\u044f HomeKit. \u0422\u0440\u0430\u043d\u0441\u043a\u043e\u0434\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0438\u043c\u0430\u0433\u0430\u0454 \u0432\u0438\u0441\u043e\u043a\u043e\u043f\u0440\u043e\u0434\u0443\u043a\u0442\u0438\u0432\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0446\u0435\u0441\u043e\u0440\u0430 \u0456 \u043d\u0430\u0432\u0440\u044f\u0434 \u0447\u0438 \u0431\u0443\u0434\u0435 \u043f\u0440\u0430\u0446\u044e\u0432\u0430\u0442\u0438 \u043d\u0430 \u043e\u0434\u043d\u043e\u043f\u043b\u0430\u0442\u043d\u0438\u0445 \u043a\u043e\u043c\u043f'\u044e\u0442\u0435\u0440\u0430\u0445.", + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0432\u0456\u0434\u0435\u043e\u043a\u043e\u0434\u0435\u043a \u043a\u0430\u043c\u0435\u0440\u0438." + }, + "include_exclude": { + "data": { + "entities": "\u0421\u0443\u0442\u043d\u043e\u0441\u0442\u0456", + "mode": "\u0420\u0435\u0436\u0438\u043c" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0443\u0442\u043d\u043e\u0441\u0442\u0456 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0456 \u0432 HomeKit. \u0423 \u0440\u0435\u0436\u0438\u043c\u0456 \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440\u0430 \u043c\u043e\u0436\u043d\u0430 \u043f\u0435\u0440\u0435\u0434\u0430\u0442\u0438 \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c. \u0423 \u0440\u0435\u0436\u0438\u043c\u0456 \u043c\u043e\u0441\u0442\u0430 \u0431\u0443\u0434\u0443\u0442\u044c \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u0456 \u0432\u0441\u0456 \u0441\u0443\u0442\u043d\u043e\u0441\u0442\u0456, \u0449\u043e \u043d\u0430\u043b\u0435\u0436\u0430\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u0443, \u044f\u043a\u0449\u043e \u043d\u0435 \u0432\u0438\u0431\u0440\u0430\u043d\u0456 \u043f\u0435\u0432\u043d\u0456 \u0441\u0443\u0442\u043d\u043e\u0441\u0442\u0456. \u0423 \u0440\u0435\u0436\u0438\u043c\u0456 \u0432\u0438\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0431\u0443\u0434\u0443\u0442\u044c \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u0456 \u0432\u0441\u0456 \u0441\u0443\u0442\u043d\u043e\u0441\u0442\u0456, \u0449\u043e \u043d\u0430\u043b\u0435\u0436\u0430\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u0443, \u043a\u0440\u0456\u043c \u0432\u0438\u0431\u0440\u0430\u043d\u0438\u0445.", + "title": "\u0412\u0438\u0431\u0456\u0440 \u0441\u0443\u0442\u043d\u043e\u0441\u0442\u0435\u0439 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0456 \u0432 HomeKit" + }, "init": { "data": { + "include_domains": "\u0412\u0438\u0431\u0440\u0430\u0442\u0438 \u0434\u043e\u043c\u0435\u043d\u0438", "mode": "\u0420\u0435\u0436\u0438\u043c" - } + }, + "description": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f \u0437 HomeKit \u043c\u043e\u0436\u0435 \u0431\u0443\u0442\u0438 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0430 \u0432 \u0440\u0435\u0436\u0438\u043c\u0456 \u043c\u043e\u0441\u0442\u0430 \u0430\u0431\u043e \u044f\u043a \u043e\u043a\u0440\u0435\u043c\u0438\u0439 \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440. \u0423 \u0440\u0435\u0436\u0438\u043c\u0456 \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440\u0430 \u043c\u043e\u0436\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u0442\u0456\u043b\u044c\u043a\u0438 \u043e\u0434\u0438\u043d \u043e\u0431'\u0454\u043a\u0442. \u041c\u0435\u0434\u0456\u0430\u043f\u043b\u0435\u0454\u0440\u0438, \u044f\u043a\u0456 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u044e\u0442\u044c\u0441\u044f \u0432 Home Assistant \u0437 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u043c 'device_class: tv', \u0434\u043b\u044f \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e\u0457 \u0440\u043e\u0431\u043e\u0442\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0431\u0443\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u043e\u0432\u0430\u043d\u0456 \u0432 Homekit \u0432 \u0440\u0435\u0436\u0438\u043c\u0456 \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440\u0430. \u041e\u0431'\u0454\u043a\u0442\u0438, \u0449\u043e \u043d\u0430\u043b\u0435\u0436\u0430\u0442\u044c \u043e\u0431\u0440\u0430\u043d\u0438\u043c \u0434\u043e\u043c\u0435\u043d\u0430\u043c, \u0431\u0443\u0434\u0443\u0442\u044c \u043f\u0435\u0440\u0435\u0434\u0430\u043d\u0456 \u0432 HomeKit. \u041d\u0430 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u043e\u043c\u0443 \u0435\u0442\u0430\u043f\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0412\u0438 \u0437\u043c\u043e\u0436\u0435\u0442\u0435 \u0432\u0438\u0431\u0440\u0430\u0442\u0438, \u044f\u043a\u0456 \u043e\u0431'\u0454\u043a\u0442\u0438 \u0432\u0438\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u0437 \u0446\u0438\u0445 \u0434\u043e\u043c\u0435\u043d\u0456\u0432.", + "title": "\u0412\u0438\u0431\u0456\u0440 \u0434\u043e\u043c\u0435\u043d\u0456\u0432 \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u0434\u0430\u0447\u0456 \u0432 HomeKit" + }, + "yaml": { + "description": "\u0426\u0435\u0439 \u0437\u0430\u043f\u0438\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e \u0447\u0435\u0440\u0435\u0437 YAML", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f HomeKit" } } } diff --git a/homeassistant/components/homekit/translations/zh-Hant.json b/homeassistant/components/homekit/translations/zh-Hant.json index 0f1093f5b5b..605263c4489 100644 --- a/homeassistant/components/homekit/translations/zh-Hant.json +++ b/homeassistant/components/homekit/translations/zh-Hant.json @@ -4,6 +4,20 @@ "port_name_in_use": "\u4f7f\u7528\u76f8\u540c\u540d\u7a31\u6216\u901a\u8a0a\u57e0\u7684\u914d\u4ef6\u6216 Bridge \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" }, "step": { + "accessory_mode": { + "data": { + "entity_id": "\u5be6\u9ad4" + }, + "description": "\u9078\u64c7\u8981\u5305\u542b\u7684\u5be6\u9ad4\u3002\u65bc\u914d\u4ef6\u6a21\u5f0f\u4e0b\uff0c\u50c5\u80fd\u5305\u542b\u55ae\u4e00\u5be6\u9ad4\u3002", + "title": "\u9078\u64c7\u8981\u5305\u542b\u7684\u5be6\u9ad4" + }, + "bridge_mode": { + "data": { + "include_domains": "\u5305\u542b\u7db2\u57df" + }, + "description": "\u9078\u64c7\u8981\u5305\u542b\u7684\u7db2\u57df\u3002\u6240\u6709\u7db2\u57df\u5167\u652f\u63f4\u7684\u5be6\u9ad4\u90fd\u6703\u5305\u542b\u3002", + "title": "\u9078\u64c7\u8981\u5305\u542b\u7684\u7db2\u57df" + }, "pairing": { "description": "\u65bc {name} \u5c31\u7dd2\u5f8c\u3001\u5c07\u6703\u65bc\u300c\u901a\u77e5\u300d\u4e2d\u986f\u793a\u300cHomeKit Bridge \u8a2d\u5b9a\u300d\u7684\u914d\u5c0d\u8cc7\u8a0a\u3002", "title": "\u914d\u5c0d HomeKit" @@ -11,9 +25,10 @@ "user": { "data": { "auto_start": "\u81ea\u52d5\u555f\u52d5\uff08\u5047\u5982\u4f7f\u7528 Z-Wave \u6216\u5176\u4ed6\u5ef6\u9072\u555f\u52d5\u7cfb\u7d71\u6642\u3001\u8acb\u95dc\u9589\uff09", - "include_domains": "\u5305\u542b Domain" + "include_domains": "\u5305\u542b\u7db2\u57df", + "mode": "\u6a21\u5f0f" }, - "description": "HomeKit \u6574\u5408\u5c07\u53ef\u5141\u8a31\u65bc Homekit \u4e2d\u4f7f\u7528 Home Assistant \u5be6\u9ad4\u3002\u65bc\u6a4b\u63a5\u6a21\u5f0f\u4e0b\u3001HomeKit Bridges \u6700\u9ad8\u9650\u5236\u70ba 150 \u500b\u914d\u4ef6\u3001\u5305\u542b Bridge \u672c\u8eab\u3002\u5047\u5982\u60f3\u8981\u4f7f\u7528\u8d85\u904e\u9650\u5236\u4ee5\u4e0a\u7684\u914d\u4ef6\uff0c\u5efa\u8b70\u53ef\u4ee5\u4e0d\u540c Domain \u4f7f\u7528\u591a\u500b HomeKit bridges \u9054\u5230\u6b64\u9700\u6c42\u3002\u50c5\u80fd\u65bc\u4e3b Bridge \u4ee5 YAML \u8a2d\u5b9a\u8a73\u7d30\u5be6\u9ad4\u3002", + "description": "HomeKit \u6574\u5408\u5c07\u53ef\u5141\u8a31\u65bc Homekit \u4e2d\u4f7f\u7528 Home Assistant \u5be6\u9ad4\u3002\u65bc\u6a4b\u63a5\u6a21\u5f0f\u4e0b\u3001HomeKit Bridges \u6700\u9ad8\u9650\u5236\u70ba 150 \u500b\u914d\u4ef6\u3001\u5305\u542b Bridge \u672c\u8eab\u3002\u5047\u5982\u60f3\u8981\u4f7f\u7528\u8d85\u904e\u9650\u5236\u4ee5\u4e0a\u7684\u914d\u4ef6\uff0c\u5efa\u8b70\u53ef\u4ee5\u4e0d\u540c\u7db2\u57df\u4f7f\u7528\u591a\u500b HomeKit bridges \u9054\u5230\u6b64\u9700\u6c42\u3002\u50c5\u80fd\u65bc\u4e3b Bridge \u4ee5 YAML \u8a2d\u5b9a\u8a73\u7d30\u5be6\u9ad4\u3002\u70ba\u53d6\u5f97\u6700\u4f73\u6548\u80fd\u3001\u4e26\u907f\u514d\u672a\u9810\u671f\u7121\u6cd5\u4f7f\u7528\u72c0\u614b\uff0c\u96fb\u8996\u5a92\u9ad4\u64ad\u653e\u5668\u8207\u651d\u5f71\u6a5f\uff0c\u8acb\u65bc Homekit \u914d\u4ef6\u6a21\u5f0f\u4e2d\u5206\u5225\u9032\u884c\u914d\u5c0d\u3002", "title": "\u555f\u7528 HomeKit" } } @@ -22,7 +37,7 @@ "step": { "advanced": { "data": { - "auto_start": "\u81ea\u52d5\u555f\u52d5\uff08\u5047\u5982\u4f7f\u7528 Z-Wave \u6216\u5176\u4ed6\u5ef6\u9072\u555f\u52d5\u7cfb\u7d71\u6642\u3001\u8acb\u95dc\u9589\uff09", + "auto_start": "\u81ea\u52d5\u555f\u52d5\uff08\u5047\u5982\u624b\u52d5\u4f7f\u7528 homekit.start \u670d\u52d9\u6642\u3001\u8acb\u95dc\u9589\uff09", "safe_mode": "\u5b89\u5168\u6a21\u5f0f\uff08\u50c5\u65bc\u914d\u5c0d\u5931\u6557\u6642\u4f7f\u7528\uff09" }, "description": "\u50c5\u65bc Homekit \u7121\u6cd5\u6b63\u5e38\u4f7f\u7528\u6642\uff0c\u8abf\u6574\u6b64\u4e9b\u8a2d\u5b9a\u3002", @@ -40,16 +55,16 @@ "entities": "\u5be6\u9ad4", "mode": "\u6a21\u5f0f" }, - "description": "\u9078\u64c7\u9032\u884c\u63a5\u901a\u7684\u5be6\u9ad4\u3002\u65bc\u5305\u542b\u6a21\u5f0f\u4e2d\u3001\u6240\u6709\u7db2\u57df\u7684\u5be6\u9ad4\u90fd\u5c07\u9032\u884c\u63a5\u901a\uff0c\u9664\u975e\u9078\u64c7\u7279\u5b9a\u7684\u5be6\u9ad4\u3002\u65bc\u6392\u9664\u6a21\u5f0f\u4e2d\u3001\u6240\u6709\u7db2\u57df\u4e2d\u7684\u9032\u884c\u63a5\u901a\uff0c\u9664\u4e86\u6392\u9664\u7684\u5be6\u9ad4\u3002", - "title": "\u9078\u64c7\u8981\u63a5\u901a\u7684\u5be6\u9ad4" + "description": "\u9078\u64c7\u8981\u5305\u542b\u7684\u5be6\u9ad4\u3002\u65bc\u914d\u4ef6\u6a21\u5f0f\u4e0b\u3001\u50c5\u6709\u55ae\u4e00\u5be6\u9ad4\u5c07\u6703\u5305\u542b\u3002\u65bc\u6a4b\u63a5\u5305\u542b\u6a21\u5f0f\u4e0b\u3001\u6240\u6709\u7db2\u57df\u7684\u5be6\u9ad4\u90fd\u5c07\u5305\u542b\uff0c\u9664\u975e\u9078\u64c7\u7279\u5b9a\u7684\u5be6\u9ad4\u3002\u65bc\u6a4b\u63a5\u6392\u9664\u6a21\u5f0f\u4e2d\u3001\u6240\u6709\u7db2\u57df\u4e2d\u7684\u5be6\u9ad4\u90fd\u5c07\u5305\u542b\uff0c\u9664\u4e86\u6392\u9664\u7684\u5be6\u9ad4\u3002\u70ba\u53d6\u5f97\u6700\u4f73\u6548\u80fd\u3001\u4e26\u907f\u514d\u672a\u9810\u671f\u7121\u6cd5\u4f7f\u7528\u72c0\u614b\uff0c\u96fb\u8996\u5a92\u9ad4\u64ad\u653e\u5668\u8207\u651d\u5f71\u6a5f\uff0c\u8acb\u65bc Homekit \u914d\u4ef6\u6a21\u5f0f\u4e2d\u5206\u5225\u9032\u884c\u914d\u5c0d\u3002", + "title": "\u9078\u64c7\u8981\u5305\u542b\u7684\u5be6\u9ad4" }, "init": { "data": { - "include_domains": "\u5305\u542b Domain", + "include_domains": "\u5305\u542b\u7db2\u57df", "mode": "\u6a21\u5f0f" }, - "description": "HomeKit \u80fd\u5920\u8a2d\u5b9a\u63a5\u901a\u6a4b\u63a5\u6216\u55ae\u4e00\u914d\u4ef6\u6a21\u5f0f\u3002\u5a92\u9ad4\u64ad\u653e\u5668\u9700\u8981\u4ee5\u96fb\u8996\u88dd\u7f6e\u914d\u4ef6\u6a21\u5f0f\u624d\u80fd\u6b63\u5e38\u4f7f\u7528\u3002\"\u5305\u542b Domains\"\u4e2d\u7684\u5be6\u9ad4\u5c07\u6703\u6a4b\u63a5\u81f3 Homekit\u3001\u53ef\u4ee5\u65bc\u4e0b\u4e00\u500b\u756b\u9762\u4e2d\u9078\u64c7\u6240\u8981\u5305\u542b\u6216\u6392\u9664\u7684\u5be6\u9ad4\u5217\u8868\u3002", - "title": "\u9078\u64c7\u6240\u8981\u63a5\u901a\u7684 Domain\u3002" + "description": "HomeKit \u80fd\u5920\u8a2d\u5b9a\u63a5\u901a\u6a4b\u63a5\u6216\u55ae\u4e00\u914d\u4ef6\u6a21\u5f0f\u3002 \u65bc\u914d\u4ef6\u6a21\u5f0f\u4e0b\u3001\u50c5\u6709\u55ae\u4e00\u5be6\u9ad4\u5c07\u6703\u5305\u542b\u3002\u5a92\u9ad4\u64ad\u653e\u5668\u9700\u8981\u4ee5\u96fb\u8996\u88dd\u7f6e\u914d\u4ef6\u6a21\u5f0f\u624d\u80fd\u6b63\u5e38\u4f7f\u7528\u3002\"\u5305\u542b\u7db2\u57df\" \u4e2d\u7684\u5be6\u9ad4\u5c07\u6703\u6a4b\u63a5\u81f3 Homekit\u3001\u53ef\u4ee5\u65bc\u4e0b\u4e00\u500b\u756b\u9762\u4e2d\u9078\u64c7\u6240\u8981\u5305\u542b\u6216\u6392\u9664\u7684\u5be6\u9ad4\u5217\u8868\u3002", + "title": "\u9078\u64c7\u6240\u8981\u5305\u542b\u7684\u7db2\u57df\u3002" }, "yaml": { "description": "\u6b64\u5be6\u9ad4\u70ba\u900f\u904e YAML \u63a7\u5236", diff --git a/homeassistant/components/homekit/type_switches.py b/homeassistant/components/homekit/type_switches.py index beaccd1f3dc..b3ee8a06497 100644 --- a/homeassistant/components/homekit/type_switches.py +++ b/homeassistant/components/homekit/type_switches.py @@ -15,9 +15,12 @@ from homeassistant.components.vacuum import ( SERVICE_RETURN_TO_BASE, SERVICE_START, STATE_CLEANING, + SUPPORT_RETURN_HOME, + SUPPORT_START, ) from homeassistant.const import ( ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, CONF_TYPE, SERVICE_TURN_OFF, SERVICE_TURN_ON, @@ -149,16 +152,24 @@ class Switch(HomeAccessory): self.char_on.set_value(current_state) -@TYPES.register("DockVacuum") -class DockVacuum(Switch): +@TYPES.register("Vacuum") +class Vacuum(Switch): """Generate a Switch accessory.""" def set_state(self, value): """Move switch state to value if call came from HomeKit.""" _LOGGER.debug("%s: Set switch state to %s", self.entity_id, value) - params = {ATTR_ENTITY_ID: self.entity_id} - service = SERVICE_START if value else SERVICE_RETURN_TO_BASE - self.call_service(VACUUM_DOMAIN, service, params) + state = self.hass.states.get(self.entity_id) + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + + if value: + sup_start = features & SUPPORT_START + service = SERVICE_START if sup_start else SERVICE_TURN_ON + else: + sup_return_home = features & SUPPORT_RETURN_HOME + service = SERVICE_RETURN_TO_BASE if sup_return_home else SERVICE_TURN_OFF + + self.call_service(VACUUM_DOMAIN, service, {ATTR_ENTITY_ID: self.entity_id}) @callback def async_update_state(self, new_state): diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index ef0fb531b1b..0a8f376fb33 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -183,6 +183,21 @@ class AccessoryEntity(HomeKitEntity): return f"homekit-{serial}-aid:{self._aid}" +class CharacteristicEntity(HomeKitEntity): + """ + A HomeKit entity that is related to an single characteristic rather than a whole service. + + This is typically used to expose additional sensor, binary_sensor or number entities that don't belong with + the service entity. + """ + + @property + def unique_id(self) -> str: + """Return the ID of this device.""" + serial = self.accessory_info.value(CharacteristicsTypes.SERIAL_NUMBER) + return f"homekit-{serial}-aid:{self._aid}-sid:{self._iid}-cid:{self._iid}" + + async def async_setup_entry(hass, entry): """Set up a HomeKit connection on a config entry.""" conn = HKDevice(hass, entry, entry.data) diff --git a/homeassistant/components/homekit_controller/connection.py b/homeassistant/components/homekit_controller/connection.py index 2b37d3e3d20..677b8dab5f6 100644 --- a/homeassistant/components/homekit_controller/connection.py +++ b/homeassistant/components/homekit_controller/connection.py @@ -15,7 +15,13 @@ from aiohomekit.model.services import ServicesTypes from homeassistant.core import callback from homeassistant.helpers.event import async_track_time_interval -from .const import CONTROLLER, DOMAIN, ENTITY_MAP, HOMEKIT_ACCESSORY_DISPATCH +from .const import ( + CHARACTERISTIC_PLATFORMS, + CONTROLLER, + DOMAIN, + ENTITY_MAP, + HOMEKIT_ACCESSORY_DISPATCH, +) from .device_trigger import async_fire_triggers, async_setup_triggers_for_entry DEFAULT_SCAN_INTERVAL = datetime.timedelta(seconds=60) @@ -82,6 +88,9 @@ class HKDevice: # A list of callbacks that turn HK service metadata into entities self.listeners = [] + # A list of callbacks that turn HK characteristics into entities + self.char_factories = [] + # The platorms we have forwarded the config entry so far. If a new # accessory is added to a bridge we may have to load additional # platforms. We don't want to load all platforms up front if its just @@ -306,6 +315,22 @@ class HKDevice: self.entities.append((accessory.aid, None)) break + def add_char_factory(self, add_entities_cb): + """Add a callback to run when discovering new entities for accessories.""" + self.char_factories.append(add_entities_cb) + self._add_new_entities_for_char([add_entities_cb]) + + def _add_new_entities_for_char(self, handlers): + for accessory in self.entity_map.accessories: + for service in accessory.services: + for char in service.characteristics: + for handler in handlers: + if (accessory.aid, service.iid, char.iid) in self.entities: + continue + if handler(char): + self.entities.append((accessory.aid, service.iid, char.iid)) + break + def add_listener(self, add_entities_cb): """Add a callback to run when discovering new entities for services.""" self.listeners.append(add_entities_cb) @@ -315,6 +340,7 @@ class HKDevice: """Process the entity map and create HA entities.""" self._add_new_entities(self.listeners) self._add_new_entities_for_accessory(self.accessory_factories) + self._add_new_entities_for_char(self.char_factories) def _add_new_entities(self, callbacks): for accessory in self.entity_map.accessories: @@ -331,26 +357,33 @@ class HKDevice: self.entities.append((aid, iid)) break + async def async_load_platform(self, platform): + """Load a single platform idempotently.""" + if platform in self.platforms: + return + + self.platforms.add(platform) + try: + await self.hass.config_entries.async_forward_entry_setup( + self.config_entry, platform + ) + except Exception: + self.platforms.remove(platform) + raise + async def async_load_platforms(self): """Load any platforms needed by this HomeKit device.""" for accessory in self.accessories: for service in accessory["services"]: stype = ServicesTypes.get_short(service["type"].upper()) - if stype not in HOMEKIT_ACCESSORY_DISPATCH: - continue + if stype in HOMEKIT_ACCESSORY_DISPATCH: + platform = HOMEKIT_ACCESSORY_DISPATCH[stype] + await self.async_load_platform(platform) - platform = HOMEKIT_ACCESSORY_DISPATCH[stype] - if platform in self.platforms: - continue - - self.platforms.add(platform) - try: - await self.hass.config_entries.async_forward_entry_setup( - self.config_entry, platform - ) - except Exception: - self.platforms.remove(platform) - raise + for char in service["characteristics"]: + if char["type"].upper() in CHARACTERISTIC_PLATFORMS: + platform = CHARACTERISTIC_PLATFORMS[char["type"].upper()] + await self.async_load_platform(platform) async def async_update(self, now=None): """Poll state of all entities attached to this bridge/accessory.""" diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index c3af1033148..a3f7a9b7921 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -1,4 +1,6 @@ """Constants for the homekit_controller component.""" +from aiohomekit.model.characteristics import CharacteristicsTypes + DOMAIN = "homekit_controller" KNOWN_DEVICES = f"{DOMAIN}-devices" @@ -40,3 +42,7 @@ HOMEKIT_ACCESSORY_DISPATCH = { "valve": "switch", "camera-rtp-stream-management": "camera", } + +CHARACTERISTIC_PLATFORMS = { + CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY: "sensor", +} diff --git a/homeassistant/components/homekit_controller/sensor.py b/homeassistant/components/homekit_controller/sensor.py index 677f1bc67f1..094d0a500d1 100644 --- a/homeassistant/components/homekit_controller/sensor.py +++ b/homeassistant/components/homekit_controller/sensor.py @@ -7,6 +7,7 @@ from homeassistant.const import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, LIGHT_LUX, PERCENTAGE, @@ -14,7 +15,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback -from . import KNOWN_DEVICES, HomeKitEntity +from . import KNOWN_DEVICES, CharacteristicEntity, HomeKitEntity HUMIDITY_ICON = "mdi:water-percent" TEMP_C_ICON = "mdi:thermometer" @@ -22,6 +23,22 @@ BRIGHTNESS_ICON = "mdi:brightness-6" CO2_ICON = "mdi:molecule-co2" +SIMPLE_SENSOR = { + CharacteristicsTypes.Vendor.EVE_ENERGY_WATT: { + "name": "Real Time Energy", + "device_class": DEVICE_CLASS_POWER, + "unit": "watts", + "icon": "mdi:chart-line", + }, + CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY: { + "name": "Real Time Energy", + "device_class": DEVICE_CLASS_POWER, + "unit": "watts", + "icon": "mdi:chart-line", + }, +} + + class HomeKitHumiditySensor(HomeKitEntity): """Representation of a Homekit humidity sensor.""" @@ -216,6 +233,66 @@ class HomeKitBatterySensor(HomeKitEntity): return self.service.value(CharacteristicsTypes.BATTERY_LEVEL) +class SimpleSensor(CharacteristicEntity): + """ + A simple sensor for a single characteristic. + + This may be an additional secondary entity that is part of another service. An + example is a switch that has an energy sensor. + + These *have* to have a different unique_id to the normal sensors as there could + be multiple entities per HomeKit service (this was not previously the case). + """ + + def __init__( + self, + conn, + info, + char, + device_class=None, + unit=None, + icon=None, + name=None, + ): + """Initialise a secondary HomeKit characteristic sensor.""" + self._device_class = device_class + self._unit = unit + self._icon = icon + self._name = name + self._char = char + + super().__init__(conn, info) + + def get_characteristic_types(self): + """Define the homekit characteristics the entity is tracking.""" + return [self._char.type] + + @property + def device_class(self): + """Return units for the sensor.""" + return self._device_class + + @property + def unit_of_measurement(self): + """Return units for the sensor.""" + return self._unit + + @property + def icon(self): + """Return the sensor icon.""" + return self._icon + + @property + def name(self) -> str: + """Return the name of the device if any.""" + return f"{super().name} - {self._name}" + + @property + def state(self): + """Return the current sensor value.""" + return self._char.value + + ENTITY_TYPES = { ServicesTypes.HUMIDITY_SENSOR: HomeKitHumiditySensor, ServicesTypes.TEMPERATURE_SENSOR: HomeKitTemperatureSensor, @@ -240,3 +317,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): return True conn.add_listener(async_add_service) + + @callback + def async_add_characteristic(char): + kwargs = SIMPLE_SENSOR.get(char.type) + if not kwargs: + return False + info = {"aid": char.service.accessory.aid, "iid": char.service.iid} + async_add_entities([SimpleSensor(conn, info, char, **kwargs)], True) + + return True + + conn.add_char_factory(async_add_characteristic) diff --git a/homeassistant/components/homekit_controller/translations/cs.json b/homeassistant/components/homekit_controller/translations/cs.json index 9a2159eda05..d5f7a502921 100644 --- a/homeassistant/components/homekit_controller/translations/cs.json +++ b/homeassistant/components/homekit_controller/translations/cs.json @@ -62,9 +62,9 @@ "doorbell": "Zvonek" }, "trigger_type": { - "double_press": "Dvakr\u00e1t stisknuto \"{subtype}\"", - "long_press": "Stisknuto a podr\u017eeno \"{subtype}\"", - "single_press": "Stisknuto \"{subtype}\"" + "double_press": "\"{subtype}\" stisknuto dvakr\u00e1t", + "long_press": "\"{subtype}\" stisknuto a podr\u017eeno", + "single_press": "\"{subtype}\" stisknuto" } }, "title": "HomeKit ovlada\u010d" diff --git a/homeassistant/components/homekit_controller/translations/de.json b/homeassistant/components/homekit_controller/translations/de.json index 3d5c538b62b..7bab8f30574 100644 --- a/homeassistant/components/homekit_controller/translations/de.json +++ b/homeassistant/components/homekit_controller/translations/de.json @@ -3,7 +3,7 @@ "abort": { "accessory_not_found_error": "Die Kopplung kann nicht durchgef\u00fchrt werden, da das Ger\u00e4t nicht mehr gefunden werden kann.", "already_configured": "Das Zubeh\u00f6r ist mit diesem Controller bereits konfiguriert.", - "already_in_progress": "Der Konfigurationsablauf f\u00fcr das Ger\u00e4t wird bereits ausgef\u00fchrt.", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "already_paired": "Dieses Zubeh\u00f6r ist bereits mit einem anderen Ger\u00e4t gekoppelt. Setze das Zubeh\u00f6r zur\u00fcck und versuche es erneut.", "ignored_model": "Die Unterst\u00fctzung von HomeKit f\u00fcr dieses Modell ist blockiert, da eine vollst\u00e4ndige native Integration verf\u00fcgbar ist.", "invalid_config_entry": "Dieses Ger\u00e4t wird als bereit zum Koppeln angezeigt, es gibt jedoch bereits einen widerspr\u00fcchlichen Konfigurationseintrag in Home Assistant, der zuerst entfernt werden muss.", @@ -30,7 +30,7 @@ "device": "Ger\u00e4t" }, "description": "W\u00e4hle das Ger\u00e4t aus, mit dem du die Kopplung herstellen m\u00f6chtest", - "title": "Mit HomeKit Zubeh\u00f6r koppeln" + "title": "Ger\u00e4teauswahl" } } }, diff --git a/homeassistant/components/homekit_controller/translations/pl.json b/homeassistant/components/homekit_controller/translations/pl.json index 50fdf6a17e4..3ccdfe452e5 100644 --- a/homeassistant/components/homekit_controller/translations/pl.json +++ b/homeassistant/components/homekit_controller/translations/pl.json @@ -62,9 +62,9 @@ "doorbell": "dzwonek do drzwi" }, "trigger_type": { - "double_press": "\"{subtype}\" naci\u015bni\u0119ty dwukrotnie", - "long_press": "\"{subtype}\" naci\u015bni\u0119ty i przytrzymany", - "single_press": "\"{subtype}\" naci\u015bni\u0119ty" + "double_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty dwukrotnie", + "long_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty i przytrzymany", + "single_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty" } }, "title": "Kontroler HomeKit" diff --git a/homeassistant/components/homekit_controller/translations/tr.json b/homeassistant/components/homekit_controller/translations/tr.json new file mode 100644 index 00000000000..9d72049ba21 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/tr.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "Aksesuar zaten bu denetleyici ile yap\u0131land\u0131r\u0131lm\u0131\u015ft\u0131r.", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor" + }, + "error": { + "authentication_error": "Yanl\u0131\u015f HomeKit kodu. L\u00fctfen kontrol edip tekrar deneyin.", + "unknown_error": "Cihaz bilinmeyen bir hata bildirdi. E\u015fle\u015ftirme ba\u015far\u0131s\u0131z oldu." + }, + "step": { + "busy_error": { + "title": "Cihaz zaten ba\u015fka bir oyun kumandas\u0131yla e\u015fle\u015fiyor" + }, + "max_tries_error": { + "title": "Maksimum kimlik do\u011frulama giri\u015fimi a\u015f\u0131ld\u0131" + } + } + }, + "device_automation": { + "trigger_subtype": { + "button1": "D\u00fc\u011fme 1", + "button10": "D\u00fc\u011fme 10", + "button2": "D\u00fc\u011fme 2", + "button3": "D\u00fc\u011fme 3", + "button4": "D\u00fc\u011fme 4", + "button5": "D\u00fc\u011fme 5", + "button6": "D\u00fc\u011fme 6", + "button7": "D\u00fc\u011fme 7", + "button8": "D\u00fc\u011fme 8", + "button9": "D\u00fc\u011fme 9", + "doorbell": "Kap\u0131 zili" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit_controller/translations/uk.json b/homeassistant/components/homekit_controller/translations/uk.json new file mode 100644 index 00000000000..66eb1741208 --- /dev/null +++ b/homeassistant/components/homekit_controller/translations/uk.json @@ -0,0 +1,71 @@ +{ + "config": { + "abort": { + "accessory_not_found_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0432\u0438\u043a\u043e\u043d\u0430\u0442\u0438 \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f, \u043e\u0441\u043a\u0456\u043b\u044c\u043a\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e.", + "already_configured": "\u0410\u043a\u0441\u0435\u0441\u0443\u0430\u0440 \u0432\u0436\u0435 \u043f\u043e\u0432'\u044f\u0437\u0430\u043d\u0438\u0439 \u0437 \u0446\u0438\u043c \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u043e\u043c.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "already_paired": "\u0426\u0435\u0439 \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440 \u0432\u0436\u0435 \u043f\u043e\u0432'\u044f\u0437\u0430\u043d\u0438\u0439 \u0437 \u0456\u043d\u0448\u0438\u043c \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0441\u043a\u0438\u043d\u044c\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440\u0430 \u0456 \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443.", + "ignored_model": "\u041f\u0456\u0434\u0442\u0440\u0438\u043c\u043a\u0430 HomeKit \u0434\u043b\u044f \u0446\u0456\u0454\u0457 \u043c\u043e\u0434\u0435\u043b\u0456 \u0437\u0430\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u0430, \u043e\u0441\u043a\u0456\u043b\u044c\u043a\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u0431\u0456\u043b\u044c\u0448 \u043f\u043e\u0432\u043d\u0430 \u043d\u0430\u0442\u0438\u0432\u043d\u0430 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f.", + "invalid_config_entry": "\u0426\u0435 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u0454\u0442\u044c\u0441\u044f \u044f\u043a \u0433\u043e\u0442\u043e\u0432\u0438\u0439 \u0434\u043e \u043e\u0431'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0432 \u043f\u0430\u0440\u0443, \u0430\u043b\u0435 \u0432 Home Assistant \u0432\u0436\u0435 \u0454 \u043a\u043e\u043d\u0444\u043b\u0456\u043a\u0442\u043d\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457 \u0434\u043b\u044f \u043d\u044c\u043e\u0433\u043e, \u044f\u043a\u0438\u0439 \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u0432\u0438\u0434\u0430\u043b\u0438\u0442\u0438.", + "invalid_properties": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0456 \u0432\u043b\u0430\u0441\u0442\u0438\u0432\u043e\u0441\u0442\u0456, \u043e\u0433\u043e\u043b\u043e\u0448\u0435\u043d\u0456 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c.", + "no_devices": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457, \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0456 \u0434\u043b\u044f \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f, \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456." + }, + "error": { + "authentication_error": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043a\u043e\u0434 HomeKit. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043a\u043e\u0434 \u0456 \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437.", + "max_peers_error": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0456\u0434\u0445\u0438\u043b\u0438\u0432 \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f \u0447\u0435\u0440\u0435\u0437 \u0432\u0456\u0434\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c \u0432\u0456\u043b\u044c\u043d\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u044f.", + "pairing_failed": "\u041f\u0456\u0434 \u0447\u0430\u0441 \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438 \u0441\u0442\u0430\u043b\u0430\u0441\u044f \u043f\u043e\u043c\u0438\u043b\u043a\u0430. \u0426\u0435 \u043c\u043e\u0436\u0435 \u0431\u0443\u0442\u0438 \u0442\u0438\u043c\u0447\u0430\u0441\u043e\u0432\u0438\u0439 \u0437\u0431\u0456\u0439 \u0430\u0431\u043e \u0412\u0430\u0448 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0430 \u0434\u0430\u043d\u0438\u0439 \u043c\u043e\u043c\u0435\u043d\u0442 \u0449\u0435 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f.", + "unable_to_pair": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043f\u0430\u0440\u0443. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437.", + "unknown_error": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u0438\u0432 \u043f\u0440\u043e \u043d\u0435\u0432\u0456\u0434\u043e\u043c\u0443 \u043f\u043e\u043c\u0438\u043b\u043a\u0443. \u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043f\u0430\u0440\u0443." + }, + "flow_title": "{name} \u0447\u0435\u0440\u0435\u0437 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440\u0456\u0432 HomeKit", + "step": { + "busy_error": { + "description": "\u0421\u043a\u0430\u0441\u0443\u0439\u0442\u0435 \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f \u043d\u0430 \u0432\u0441\u0456\u0445 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430\u0445 \u0430\u0431\u043e \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439, \u043f\u043e\u0442\u0456\u043c \u043f\u043e\u0432\u0442\u043e\u0440\u0456\u0442\u044c \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f.", + "title": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0443\u0436\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0434\u043e \u0456\u043d\u0448\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430." + }, + "max_tries_error": { + "description": "\u041f\u043e\u043d\u0430\u0434 100 \u0441\u043f\u0440\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457 \u043f\u0440\u043e\u0439\u0448\u043b\u0438 \u043d\u0435\u0432\u0434\u0430\u043b\u043e. \u0421\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439, \u0430 \u043f\u043e\u0442\u0456\u043c \u043f\u043e\u0432\u0442\u043e\u0440\u0456\u0442\u044c \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f.", + "title": "\u041f\u0435\u0440\u0435\u0432\u0438\u0449\u0435\u043d\u043e \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0443 \u043a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u0441\u043f\u0440\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457." + }, + "pair": { + "data": { + "pairing_code": "\u041a\u043e\u0434 \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f" + }, + "description": "HomeKit Controller \u043e\u0431\u043c\u0456\u043d\u044e\u0454\u0442\u044c\u0441\u044f \u0434\u0430\u043d\u0438\u043c\u0438 \u0437 {name} \u043f\u043e \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u0456\u0439 \u043c\u0435\u0440\u0435\u0436\u0456, \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u044e\u0447\u0438 \u0431\u0435\u0437\u043f\u0435\u0447\u043d\u0435 \u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u0435 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0431\u0435\u0437 \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e\u0441\u0442\u0456 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f \u043e\u043a\u0440\u0435\u043c\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430 HomeKit \u0430\u0431\u043e iCloud. \u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043a\u043e\u0434 \u0441\u043f\u043e\u043b\u0443\u0447\u0435\u043d\u043d\u044f HomeKit (\u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 XXX-XX-XXX), \u0449\u043e\u0431 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0446\u0435\u0439 \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440. \u0426\u0435\u0439 \u043a\u043e\u0434 \u0437\u0430\u0437\u0432\u0438\u0447\u0430\u0439 \u0437\u043d\u0430\u0445\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u043d\u0430 \u0441\u0430\u043c\u043e\u043c\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u0430\u0431\u043e \u043d\u0430 \u0443\u043f\u0430\u043a\u043e\u0432\u0446\u0456.", + "title": "\u0421\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438 \u0437 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c \u0447\u0435\u0440\u0435\u0437 \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440\u0456\u0432 HomeKit" + }, + "protocol_error": { + "description": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439, \u043c\u043e\u0436\u043b\u0438\u0432\u043e, \u043d\u0435 \u0437\u043d\u0430\u0445\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0432 \u0440\u0435\u0436\u0438\u043c\u0456 \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f \u0456 \u043c\u043e\u0436\u0435 \u0437\u043d\u0430\u0434\u043e\u0431\u0438\u0442\u0438\u0441\u044f \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f \u0444\u0456\u0437\u0438\u0447\u043d\u043e\u0457 \u0430\u0431\u043e \u0432\u0456\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u043e\u0457 \u043a\u043d\u043e\u043f\u043a\u0438. \u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0437\u043d\u0430\u0445\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0432 \u0440\u0435\u0436\u0438\u043c\u0456 \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438, \u0430\u0431\u043e \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439, \u0456 \u043f\u043e\u0442\u0456\u043c \u043f\u043e\u0432\u0442\u043e\u0440\u0456\u0442\u044c \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f.", + "title": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437\u0432'\u044f\u0437\u043a\u0443 \u0437 \u0430\u043a\u0441\u0435\u0441\u0443\u0430\u0440\u043e\u043c." + }, + "user": { + "data": { + "device": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + }, + "description": "HomeKit Controller \u043e\u0431\u043c\u0456\u043d\u044e\u0454\u0442\u044c\u0441\u044f \u0434\u0430\u043d\u0438\u043c\u0438 \u0432 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u0456\u0439 \u043c\u0435\u0440\u0435\u0436\u0456 \u0437 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f\u043c \u0431\u0435\u0437\u043f\u0435\u0447\u043d\u043e\u0433\u043e \u0437\u0430\u0448\u0438\u0444\u0440\u043e\u0432\u0430\u043d\u043e\u0433\u043e \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0431\u0435\u0437 \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e\u0441\u0442\u0456 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f \u043e\u043a\u0440\u0435\u043c\u043e\u0433\u043e \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430 HomeKit \u0430\u0431\u043e iCloud. \u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439, \u0437 \u044f\u043a\u0438\u043c \u0445\u043e\u0447\u0435\u0442\u0435 \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043f\u0430\u0440\u0443:", + "title": "\u0412\u0438\u0431\u0456\u0440 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + }, + "device_automation": { + "trigger_subtype": { + "button1": "\u041a\u043d\u043e\u043f\u043a\u0430 1", + "button10": "\u041a\u043d\u043e\u043f\u043a\u0430 10", + "button2": "\u041a\u043d\u043e\u043f\u043a\u0430 2", + "button3": "\u041a\u043d\u043e\u043f\u043a\u0430 3", + "button4": "\u041a\u043d\u043e\u043f\u043a\u0430 4", + "button5": "\u041a\u043d\u043e\u043f\u043a\u0430 5", + "button6": "\u041a\u043d\u043e\u043f\u043a\u0430 6", + "button7": "\u041a\u043d\u043e\u043f\u043a\u0430 7", + "button8": "\u041a\u043d\u043e\u043f\u043a\u0430 8", + "button9": "\u041a\u043d\u043e\u043f\u043a\u0430 9", + "doorbell": "\u0414\u0432\u0435\u0440\u043d\u0438\u0439 \u0434\u0437\u0432\u0456\u043d\u043e\u043a" + }, + "trigger_type": { + "double_press": "\"{subtype}\" \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0434\u0432\u0456\u0447\u0456", + "long_press": "\"{subtype}\" \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0456 \u0443\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f", + "single_press": "\"{subtype}\" \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430" + } + }, + "title": "HomeKit Controller" +} \ No newline at end of file diff --git a/homeassistant/components/homematic/const.py b/homeassistant/components/homematic/const.py index cd474428113..a6ff19a6eea 100644 --- a/homeassistant/components/homematic/const.py +++ b/homeassistant/components/homematic/const.py @@ -46,6 +46,7 @@ HM_DEVICE_TYPES = { "Switch", "SwitchPowermeter", "IOSwitch", + "IOSwitchNoInhibit", "IPSwitch", "RFSiren", "IPSwitchPowermeter", @@ -115,6 +116,10 @@ HM_DEVICE_TYPES = { "IPRemoteMotionV2", "HBUNISenWEA", "PresenceIPW", + "IPRainSensor", + "ValveBox", + "IPKeyBlind", + "IPKeyBlindTilt", ], DISCOVER_CLIMATE: [ "Thermostat", @@ -158,6 +163,7 @@ HM_DEVICE_TYPES = { "IPWInputDevice", "IPWMotionDection", "IPAlarmSensor", + "IPRainSensor", ], DISCOVER_COVER: [ "Blind", diff --git a/homeassistant/components/homematic/manifest.json b/homeassistant/components/homematic/manifest.json index 63e33a60c53..36414b606f9 100644 --- a/homeassistant/components/homematic/manifest.json +++ b/homeassistant/components/homematic/manifest.json @@ -2,6 +2,6 @@ "domain": "homematic", "name": "Homematic", "documentation": "https://www.home-assistant.io/integrations/homematic", - "requirements": ["pyhomematic==0.1.70"], + "requirements": ["pyhomematic==0.1.71"], "codeowners": ["@pvizeli", "@danielperna84"] } diff --git a/homeassistant/components/homematicip_cloud/binary_sensor.py b/homeassistant/components/homematicip_cloud/binary_sensor.py index 57aaa2b0b01..4f1ae523ecc 100644 --- a/homeassistant/components/homematicip_cloud/binary_sensor.py +++ b/homeassistant/components/homematicip_cloud/binary_sensor.py @@ -12,6 +12,7 @@ from homematicip.aio.device import ( AsyncMotionDetectorPushButton, AsyncPluggableMainsFailureSurveillance, AsyncPresenceDetectorIndoor, + AsyncRainSensor, AsyncRotaryHandleSensor, AsyncShutterContact, AsyncShutterContactMagnetic, @@ -127,7 +128,9 @@ async def async_setup_entry( entities.append(HomematicipSmokeDetector(hap, device)) if isinstance(device, AsyncWaterSensor): entities.append(HomematicipWaterDetector(hap, device)) - if isinstance(device, (AsyncWeatherSensorPlus, AsyncWeatherSensorPro)): + if isinstance( + device, (AsyncRainSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro) + ): entities.append(HomematicipRainSensor(hap, device)) if isinstance( device, (AsyncWeatherSensor, AsyncWeatherSensorPlus, AsyncWeatherSensorPro) diff --git a/homeassistant/components/homematicip_cloud/manifest.json b/homeassistant/components/homematicip_cloud/manifest.json index 9f045694460..93e96267be3 100644 --- a/homeassistant/components/homematicip_cloud/manifest.json +++ b/homeassistant/components/homematicip_cloud/manifest.json @@ -3,7 +3,7 @@ "name": "HomematicIP Cloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homematicip_cloud", - "requirements": ["homematicip==0.13.0"], + "requirements": ["homematicip==0.13.1"], "codeowners": ["@SukramJ"], "quality_scale": "platinum" } diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 9047ed9095f..f8c37d336d5 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -3,6 +3,7 @@ from typing import Any, Dict from homematicip.aio.device import ( AsyncBrandSwitchMeasuring, + AsyncDinRailSwitch, AsyncDinRailSwitch4, AsyncFullFlushInputSwitch, AsyncFullFlushSwitchMeasuring, @@ -45,6 +46,8 @@ async def async_setup_entry( elif isinstance(device, AsyncWiredSwitch8): for channel in range(1, 9): entities.append(HomematicipMultiSwitch(hap, device, channel=channel)) + elif isinstance(device, AsyncDinRailSwitch): + entities.append(HomematicipMultiSwitch(hap, device, channel=1)) elif isinstance(device, AsyncDinRailSwitch4): for channel in range(1, 5): entities.append(HomematicipMultiSwitch(hap, device, channel=channel)) diff --git a/homeassistant/components/homematicip_cloud/translations/de.json b/homeassistant/components/homematicip_cloud/translations/de.json index c421620fd98..1da1e06c0fb 100644 --- a/homeassistant/components/homematicip_cloud/translations/de.json +++ b/homeassistant/components/homematicip_cloud/translations/de.json @@ -2,11 +2,11 @@ "config": { "abort": { "already_configured": "Der Accesspoint ist bereits konfiguriert", - "connection_aborted": "Konnte nicht mit HMIP Server verbinden", - "unknown": "Ein unbekannter Fehler ist aufgetreten." + "connection_aborted": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" }, "error": { - "invalid_sgtin_or_pin": "Ung\u00fcltige PIN, bitte versuche es erneut.", + "invalid_sgtin_or_pin": "Ung\u00fcltige SGTIN oder PIN-Code, bitte versuche es erneut.", "press_the_button": "Bitte dr\u00fccke die blaue Taste.", "register_failed": "Registrierung fehlgeschlagen, bitte versuche es erneut.", "timeout_button": "Zeit\u00fcberschreitung beim Dr\u00fccken der blauen Taste. Bitte versuche es erneut." @@ -16,7 +16,7 @@ "data": { "hapid": "Accesspoint ID (SGTIN)", "name": "Name (optional, wird als Pr\u00e4fix f\u00fcr alle Ger\u00e4te verwendet)", - "pin": "PIN Code (optional)" + "pin": "PIN-Code" }, "title": "HomematicIP Accesspoint ausw\u00e4hlen" }, diff --git a/homeassistant/components/homematicip_cloud/translations/tr.json b/homeassistant/components/homematicip_cloud/translations/tr.json new file mode 100644 index 00000000000..72f139217ca --- /dev/null +++ b/homeassistant/components/homematicip_cloud/translations/tr.json @@ -0,0 +1,9 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "connection_aborted": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homematicip_cloud/translations/uk.json b/homeassistant/components/homematicip_cloud/translations/uk.json new file mode 100644 index 00000000000..1ed2e317f8b --- /dev/null +++ b/homeassistant/components/homematicip_cloud/translations/uk.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "connection_aborted": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "invalid_sgtin_or_pin": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 SGTIN \u0430\u0431\u043e PIN-\u043a\u043e\u0434, \u0431\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443.", + "press_the_button": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u0441\u0438\u043d\u044e \u043a\u043d\u043e\u043f\u043a\u0443.", + "register_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0432\u0430\u0442\u0438\u0441\u044f, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443.", + "timeout_button": "\u0412\u0438 \u043d\u0435 \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u043b\u0438 \u043d\u0430 \u0441\u0438\u043d\u044e \u043a\u043d\u043e\u043f\u043a\u0443 \u0432 \u043c\u0435\u0436\u0430\u0445 \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e\u0433\u043e \u0447\u0430\u0441\u0443, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443." + }, + "step": { + "init": { + "data": { + "hapid": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0442\u043e\u0447\u043a\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u0443 (SGTIN)", + "name": "\u041d\u0430\u0437\u0432\u0430 (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e, \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u044f\u043a \u043f\u0440\u0435\u0444\u0456\u043a\u0441 \u0434\u043b\u044f \u043d\u0430\u0437\u0432\u0438 \u0432\u0441\u0456\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432)", + "pin": "PIN-\u043a\u043e\u0434" + }, + "title": "HomematicIP Cloud" + }, + "link": { + "description": "\u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u0441\u0438\u043d\u044e \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0442\u043e\u0447\u0446\u0456 \u0434\u043e\u0441\u0442\u0443\u043f\u0443 \u0456 \u043a\u043d\u043e\u043f\u043a\u0443 ** \u041f\u0406\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u0418 **, \u0449\u043e\u0431 \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0432\u0430\u0442\u0438 HomematicIP \u0432 Home Assistant. \n\n![\u0420\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f \u043a\u043d\u043e\u043f\u043a\u0438] (/static/images/config_flows/config_homematicip_cloud.png)", + "title": "\u041f\u0440\u0438\u0432'\u044f\u0437\u0430\u0442\u0438 \u0442\u043e\u0447\u043a\u0443 \u0434\u043e\u0441\u0442\u0443\u043f\u0443" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 0e7958a3ea1..4b87350aec8 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -53,6 +53,8 @@ CONF_LOC_ID = "location" DEFAULT_COOL_AWAY_TEMPERATURE = 88 DEFAULT_HEAT_AWAY_TEMPERATURE = 61 +ATTR_PERMANENT_HOLD = "permanent_hold" + PLATFORM_SCHEMA = vol.All( cv.deprecated(CONF_REGION), PLATFORM_SCHEMA.extend( @@ -199,6 +201,7 @@ class HoneywellUSThermostat(ClimateEntity): """Return the device specific state attributes.""" data = {} data[ATTR_FAN_ACTION] = "running" if self._device.fan_running else "idle" + data[ATTR_PERMANENT_HOLD] = self._is_permanent_hold() if self._device.raw_dr_data: data["dr_phase"] = self._device.raw_dr_data.get("Phase") return data @@ -306,6 +309,11 @@ class HoneywellUSThermostat(ClimateEntity): """Return the list of available fan modes.""" return list(self._fan_mode_map) + def _is_permanent_hold(self) -> bool: + heat_status = self._device.raw_ui_data.get("StatusHeat", 0) + cool_status = self._device.raw_ui_data.get("StatusCool", 0) + return heat_status == 2 or cool_status == 2 + def _set_temperature(self, **kwargs) -> None: """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) diff --git a/homeassistant/components/http/ban.py b/homeassistant/components/http/ban.py index 14d81a1eb6e..2e51dc35d88 100644 --- a/homeassistant/components/http/ban.py +++ b/homeassistant/components/http/ban.py @@ -105,16 +105,18 @@ async def process_wrong_login(request): except herror: pass - msg = f"Login attempt or request with invalid authentication from {remote_host} ({remote_addr})" + base_msg = f"Login attempt or request with invalid authentication from {remote_host} ({remote_addr})." + # The user-agent is unsanitized input so we only include it in the log user_agent = request.headers.get("user-agent") - if user_agent: - msg = f"{msg} ({user_agent})" + log_msg = f"{base_msg} ({user_agent})" - _LOGGER.warning(msg) + notification_msg = f"{base_msg} See the log for details." + + _LOGGER.warning(log_msg) hass.components.persistent_notification.async_create( - msg, "Login attempt failed", NOTIFICATION_ID_LOGIN + notification_msg, "Login attempt failed", NOTIFICATION_ID_LOGIN ) # Check if ban middleware is loaded diff --git a/homeassistant/components/http/data_validator.py b/homeassistant/components/http/data_validator.py index 8f3e9a3e1e2..d63912360a2 100644 --- a/homeassistant/components/http/data_validator.py +++ b/homeassistant/components/http/data_validator.py @@ -16,7 +16,7 @@ _LOGGER = logging.getLogger(__name__) class RequestDataValidator: """Decorator that will validate the incoming data. - Takes in a voluptuous schema and adds 'post_data' as + Takes in a voluptuous schema and adds 'data' as keyword argument to the function call. Will return a 400 if no JSON provided or doesn't match schema. diff --git a/homeassistant/components/http/web_runner.py b/homeassistant/components/http/web_runner.py index 67621d63412..c30ba32b780 100644 --- a/homeassistant/components/http/web_runner.py +++ b/homeassistant/components/http/web_runner.py @@ -28,7 +28,7 @@ class HomeAssistantTCPSite(web.BaseSite): host: Union[None, str, List[str]], port: int, *, - shutdown_timeout: float = 60.0, + shutdown_timeout: float = 10.0, ssl_context: Optional[SSLContext] = None, backlog: int = 128, reuse_address: Optional[bool] = None, diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index c341f75c2e5..341f9c0a118 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -15,6 +15,7 @@ from huawei_lte_api.AuthorizedConnection import AuthorizedConnection from huawei_lte_api.Client import Client from huawei_lte_api.Connection import Connection from huawei_lte_api.exceptions import ( + ResponseErrorException, ResponseErrorLoginRequiredException, ResponseErrorNotSupportedException, ) @@ -208,6 +209,14 @@ class Router: "%s requires authorization, excluding from future updates", key ) self.subscriptions.pop(key) + except ResponseErrorException as exc: + if exc.code != -1: + raise + _LOGGER.info( + "%s apparently not supported by device, excluding from future updates", + key, + ) + self.subscriptions.pop(key) except Timeout: grace_left = ( self.notify_last_attempt - time.monotonic() + NOTIFY_SUPPRESS_TIMEOUT diff --git a/homeassistant/components/huawei_lte/translations/de.json b/homeassistant/components/huawei_lte/translations/de.json index 7da997f12d6..43361e46929 100644 --- a/homeassistant/components/huawei_lte/translations/de.json +++ b/homeassistant/components/huawei_lte/translations/de.json @@ -1,14 +1,15 @@ { "config": { "abort": { - "already_configured": "Dieses Ger\u00e4t wurde bereits konfiguriert", - "already_in_progress": "Dieses Ger\u00e4t wurde bereits konfiguriert", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "not_huawei_lte": "Kein Huawei LTE-Ger\u00e4t" }, "error": { "connection_timeout": "Verbindungszeit\u00fcberschreitung", "incorrect_password": "Ung\u00fcltiges Passwort", "incorrect_username": "Ung\u00fcltiger Benutzername", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "invalid_url": "Ung\u00fcltige URL", "login_attempts_exceeded": "Maximale Anzahl von Anmeldeversuchen \u00fcberschritten. Bitte versuche es sp\u00e4ter erneut", "response_error": "Unbekannter Fehler vom Ger\u00e4t", diff --git a/homeassistant/components/huawei_lte/translations/tr.json b/homeassistant/components/huawei_lte/translations/tr.json index a76e31fa483..ba934acc39b 100644 --- a/homeassistant/components/huawei_lte/translations/tr.json +++ b/homeassistant/components/huawei_lte/translations/tr.json @@ -1,8 +1,35 @@ { + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor" + }, + "error": { + "connection_timeout": "Ba\u011flant\u0131 zamana\u015f\u0131m\u0131", + "incorrect_password": "Yanl\u0131\u015f parola", + "incorrect_username": "Yanl\u0131\u015f kullan\u0131c\u0131 ad\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_url": "Ge\u00e7ersiz URL", + "login_attempts_exceeded": "Maksimum oturum a\u00e7ma denemesi a\u015f\u0131ld\u0131, l\u00fctfen daha sonra tekrar deneyin", + "response_error": "Cihazdan bilinmeyen hata", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "url": "URL", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "Cihaz eri\u015fim ayr\u0131nt\u0131lar\u0131n\u0131 girin. Kullan\u0131c\u0131 ad\u0131 ve parolan\u0131n belirtilmesi iste\u011fe ba\u011fl\u0131d\u0131r, ancak daha fazla entegrasyon \u00f6zelli\u011fi i\u00e7in destek sa\u011flar. \u00d6te yandan, yetkili bir ba\u011flant\u0131n\u0131n kullan\u0131lmas\u0131, entegrasyon aktifken Ev Asistan\u0131 d\u0131\u015f\u0131ndan cihaz web aray\u00fcz\u00fcne eri\u015fimde sorunlara neden olabilir ve tam tersi." + } + } + }, "options": { "step": { "init": { "data": { + "recipient": "SMS bildirimi al\u0131c\u0131lar\u0131", "track_new_devices": "Yeni cihazlar\u0131 izle" } } diff --git a/homeassistant/components/huawei_lte/translations/uk.json b/homeassistant/components/huawei_lte/translations/uk.json new file mode 100644 index 00000000000..17f3d3b71c3 --- /dev/null +++ b/homeassistant/components/huawei_lte/translations/uk.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "not_huawei_lte": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0454 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c Huawei LTE" + }, + "error": { + "connection_timeout": "\u0427\u0430\u0441 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u043c\u0438\u043d\u0443\u0432.", + "incorrect_password": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439 \u043f\u0430\u0440\u043e\u043b\u044c.", + "incorrect_username": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0435 \u0456\u043c\u2019\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "invalid_url": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0430 URL-\u0430\u0434\u0440\u0435\u0441\u0430.", + "login_attempts_exceeded": "\u041f\u0435\u0440\u0435\u0432\u0438\u0449\u0435\u043d\u043e \u043c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0443 \u043a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u0441\u043f\u0440\u043e\u0431 \u0432\u0445\u043e\u0434\u0443, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437 \u043f\u0456\u0437\u043d\u0456\u0448\u0435.", + "response_error": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "flow_title": "Huawei LTE: {name}", + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "url": "URL-\u0430\u0434\u0440\u0435\u0441\u0430", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0434\u0430\u043d\u0456 \u0434\u043b\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e. \u0412\u043a\u0430\u0437\u0443\u0432\u0430\u0442\u0438 \u043b\u043e\u0433\u0456\u043d \u0456 \u043f\u0430\u0440\u043e\u043b\u044c \u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e, \u0430\u043b\u0435 \u0446\u0435 \u0434\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u044c \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0456 \u0444\u0443\u043d\u043a\u0446\u0456\u0457. \u0417 \u0456\u043d\u0448\u043e\u0433\u043e \u0431\u043e\u043a\u0443, \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u043e\u0433\u043e \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u043c\u043e\u0436\u0435 \u0432\u0438\u043a\u043b\u0438\u043a\u0430\u0442\u0438 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u0437 \u0434\u043e\u0441\u0442\u0443\u043f\u043e\u043c \u0434\u043e \u0432\u0435\u0431-\u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u043d\u0435 \u0437 Home Assistant, \u043a\u043e\u043b\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f \u0430\u043a\u0442\u0438\u0432\u043d\u0430, \u0456 \u043d\u0430\u0432\u043f\u0430\u043a\u0438.", + "title": "Huawei LTE" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430 \u0441\u043b\u0443\u0436\u0431\u0438 \u0441\u043f\u043e\u0432\u0456\u0449\u0435\u043d\u044c (\u043f\u043e\u0442\u0440\u0456\u0431\u0435\u043d \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a)", + "recipient": "\u041e\u0434\u0435\u0440\u0436\u0443\u0432\u0430\u0447\u0456 SMS-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c", + "track_new_devices": "\u0412\u0456\u0434\u0441\u0442\u0435\u0436\u0443\u0432\u0430\u0442\u0438 \u043d\u043e\u0432\u0456 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/helpers.py b/homeassistant/components/hue/helpers.py index 885677dc269..1760c59a69d 100644 --- a/homeassistant/components/hue/helpers.py +++ b/homeassistant/components/hue/helpers.py @@ -22,9 +22,7 @@ async def remove_devices(bridge, api_ids, current): if entity.entity_id in ent_registry.entities: ent_registry.async_remove(entity.entity_id) dev_registry = await get_dev_reg(bridge.hass) - device = dev_registry.async_get_device( - identifiers={(DOMAIN, entity.device_id)}, connections=set() - ) + device = dev_registry.async_get_device(identifiers={(DOMAIN, entity.device_id)}) if device is not None: dev_registry.async_update_device( device.id, remove_config_entry_id=bridge.config_entry.entry_id diff --git a/homeassistant/components/hue/translations/cs.json b/homeassistant/components/hue/translations/cs.json index 76606338320..1708abfe750 100644 --- a/homeassistant/components/hue/translations/cs.json +++ b/homeassistant/components/hue/translations/cs.json @@ -48,7 +48,7 @@ }, "trigger_type": { "remote_button_long_release": "Tla\u010d\u00edtko \"{subtype}\" uvoln\u011bno po dlouh\u00e9m stisku", - "remote_button_short_press": "Stisknuto tla\u010d\u00edtko \"{subtype}\"", + "remote_button_short_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto", "remote_button_short_release": "Uvoln\u011bno tla\u010d\u00edtko \"{subtype}\"", "remote_double_button_long_press": "Oba \"{subtype}\" uvoln\u011bny po dlouh\u00e9m stisku", "remote_double_button_short_press": "Oba \"{subtype}\" uvoln\u011bny" diff --git a/homeassistant/components/hue/translations/de.json b/homeassistant/components/hue/translations/de.json index 0defb33ae5e..122e1ba6f5c 100644 --- a/homeassistant/components/hue/translations/de.json +++ b/homeassistant/components/hue/translations/de.json @@ -1,17 +1,17 @@ { "config": { "abort": { - "all_configured": "Alle Philips Hue Bridges sind bereits konfiguriert", - "already_configured": "Bridge ist bereits konfiguriert", - "already_in_progress": "Der Konfigurationsablauf f\u00fcr die Bridge wird bereits ausgef\u00fchrt.", - "cannot_connect": "Verbindung zur Bridge nicht m\u00f6glich", - "discover_timeout": "Nicht in der Lage Hue Bridges zu entdecken", - "no_bridges": "Keine Philips Hue Bridges entdeckt", + "all_configured": "Es sind bereits alle Philips Hue Bridges konfiguriert", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "cannot_connect": "Verbindung fehlgeschlagen", + "discover_timeout": "Es k\u00f6nnen keine Hue Bridges erkannt werden", + "no_bridges": "Keine Philips Hue Bridges erkannt", "not_hue_bridge": "Keine Philips Hue Bridge entdeckt", "unknown": "Unbekannter Fehler ist aufgetreten" }, "error": { - "linking": "Unbekannter Link-Fehler aufgetreten.", + "linking": "Unerwarteter Fehler", "register_failed": "Registrieren fehlgeschlagen, bitte versuche es erneut" }, "step": { @@ -22,7 +22,7 @@ "title": "W\u00e4hle eine Hue Bridge" }, "link": { - "description": "Dr\u00fccke den Knopf auf der Bridge, um Philips Hue mit Home Assistant zu registrieren.\n\n![Position des Buttons auf der Bridge](/static/images/config_philips_hue.jpg)", + "description": "Dr\u00fccke den Knopf auf der Bridge, um Philips Hue mit Home Assistant zu verkn\u00fcpfen.\n\n![Position des Buttons auf der Bridge](/static/images/config_philips_hue.jpg)", "title": "Hub verbinden" }, "manual": { @@ -58,8 +58,8 @@ "step": { "init": { "data": { - "allow_hue_groups": "Erlaube Hue Gruppen", - "allow_unreachable": "Erlauben Sie unerreichbaren Gl\u00fchbirnen, ihren Zustand korrekt zu melden" + "allow_hue_groups": "Hue-Gruppen erlauben", + "allow_unreachable": "Erlaube nicht erreichbaren Gl\u00fchlampen, ihren Zustand korrekt zu melden" } } } diff --git a/homeassistant/components/hue/translations/pl.json b/homeassistant/components/hue/translations/pl.json index 873f60946d5..b144393c3d1 100644 --- a/homeassistant/components/hue/translations/pl.json +++ b/homeassistant/components/hue/translations/pl.json @@ -47,11 +47,11 @@ "turn_on": "w\u0142\u0105cznik" }, "trigger_type": { - "remote_button_long_release": "\"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", - "remote_button_short_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty", - "remote_button_short_release": "\"{subtype}\" zostanie zwolniony", - "remote_double_button_long_press": "oba \"{subtype}\" zostan\u0105 zwolnione po d\u0142ugim naci\u015bni\u0119ciu", - "remote_double_button_short_press": "oba \"{subtype}\" zostan\u0105 zwolnione" + "remote_button_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "remote_button_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty", + "remote_button_short_release": "przycisk \"{subtype}\" zostanie zwolniony", + "remote_double_button_long_press": "oba przyciski \"{subtype}\" zostan\u0105 zwolnione po d\u0142ugim naci\u015bni\u0119ciu", + "remote_double_button_short_press": "oba przyciski \"{subtype}\" zostan\u0105 zwolnione" } }, "options": { diff --git a/homeassistant/components/hue/translations/pt.json b/homeassistant/components/hue/translations/pt.json index 8eabbbb08cc..09d839cbd5c 100644 --- a/homeassistant/components/hue/translations/pt.json +++ b/homeassistant/components/hue/translations/pt.json @@ -2,16 +2,16 @@ "config": { "abort": { "all_configured": "Todos os hubs Philips Hue j\u00e1 est\u00e3o configurados", - "already_configured": "Hue j\u00e1 est\u00e1 configurado", + "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", - "cannot_connect": "N\u00e3o foi poss\u00edvel conectar-se ao hub", + "cannot_connect": "Falha na liga\u00e7\u00e3o", "discover_timeout": "Nenhum hub Hue descoberto", "no_bridges": "Nenhum hub Philips Hue descoberto", "not_hue_bridge": "N\u00e3o \u00e9 uma bridge Hue", - "unknown": "Ocorreu um erro desconhecido" + "unknown": "Erro inesperado" }, "error": { - "linking": "Ocorreu um erro de liga\u00e7\u00e3o desconhecido.", + "linking": "Erro inesperado", "register_failed": "Falha ao registar, por favor, tente novamente" }, "step": { diff --git a/homeassistant/components/hue/translations/tr.json b/homeassistant/components/hue/translations/tr.json new file mode 100644 index 00000000000..984c91e8f36 --- /dev/null +++ b/homeassistant/components/hue/translations/tr.json @@ -0,0 +1,48 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "error": { + "linking": "Beklenmeyen hata" + }, + "step": { + "init": { + "data": { + "host": "Ana Bilgisayar" + } + }, + "manual": { + "data": { + "host": "Ana Bilgisayar" + }, + "title": "Bir Hue k\u00f6pr\u00fcs\u00fcn\u00fc manuel olarak yap\u0131land\u0131rma" + } + } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "\u0130lk d\u00fc\u011fme", + "button_2": "\u0130kinci d\u00fc\u011fme", + "button_3": "\u00dc\u00e7\u00fcnc\u00fc d\u00fc\u011fme", + "button_4": "D\u00f6rd\u00fcnc\u00fc d\u00fc\u011fme", + "double_buttons_1_3": "Birinci ve \u00dc\u00e7\u00fcnc\u00fc d\u00fc\u011fmeler", + "double_buttons_2_4": "\u0130kinci ve D\u00f6rd\u00fcnc\u00fc d\u00fc\u011fmeler", + "turn_off": "Kapat", + "turn_on": "A\u00e7" + } + }, + "options": { + "step": { + "init": { + "data": { + "allow_hue_groups": "Hue gruplar\u0131na izin ver", + "allow_unreachable": "Ula\u015f\u0131lamayan ampullerin durumlar\u0131n\u0131 do\u011fru \u015fekilde bildirmesine izin verin" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/translations/uk.json b/homeassistant/components/hue/translations/uk.json new file mode 100644 index 00000000000..8e9c5ca82cb --- /dev/null +++ b/homeassistant/components/hue/translations/uk.json @@ -0,0 +1,67 @@ +{ + "config": { + "abort": { + "all_configured": "\u0412\u0441\u0456 \u0448\u043b\u044e\u0437\u0438 Philips Hue \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0456.", + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "discover_timeout": "\u0428\u043b\u044e\u0437 Philips Hue \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e.", + "no_bridges": "\u0428\u043b\u044e\u0437\u0438 Philips Hue \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456.", + "not_hue_bridge": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0454 \u0448\u043b\u044e\u0437\u043e\u043c Hue.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "linking": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430", + "register_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0432\u0430\u0442\u0438\u0441\u044f, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443." + }, + "step": { + "init": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0448\u043b\u044e\u0437 Hue" + }, + "link": { + "description": "\u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0448\u043b\u044e\u0437\u0456 \u0434\u043b\u044f \u0440\u0435\u0454\u0441\u0442\u0440\u0430\u0446\u0456\u0457 Philips Hue \u0432 Home Assistant. \n\n![\u0420\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f \u043a\u043d\u043e\u043f\u043a\u0438 \u043d\u0430 \u0448\u043b\u044e\u0437\u0456] (/static/images/config_philips_hue.jpg)", + "title": "Philips Hue" + }, + "manual": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "title": "\u0420\u0443\u0447\u043d\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0448\u043b\u044e\u0437\u0443" + } + } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "\u041f\u0435\u0440\u0448\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "button_2": "\u0414\u0440\u0443\u0433\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "button_3": "\u0422\u0440\u0435\u0442\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "dim_down": "\u0417\u043c\u0435\u043d\u0448\u0438\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c", + "dim_up": "\u0417\u0431\u0456\u043b\u044c\u0448\u0438\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c", + "double_buttons_1_3": "\u041f\u0435\u0440\u0448\u0430 \u0456 \u0442\u0440\u0435\u0442\u044f \u043a\u043d\u043e\u043f\u043a\u0438", + "double_buttons_2_4": "\u0414\u0440\u0443\u0433\u0430 \u0456 \u0447\u0435\u0442\u0432\u0435\u0440\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0438", + "turn_off": "\u0412\u0438\u043c\u043a\u043d\u0443\u0442\u0438", + "turn_on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438" + }, + "trigger_type": { + "remote_button_long_release": "{subtype} \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0456\u0441\u043b\u044f \u0434\u043e\u0432\u0433\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f", + "remote_button_short_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430", + "remote_button_short_release": "{subtype} \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0456\u0441\u043b\u044f \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f", + "remote_double_button_long_press": "{subtype} \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u043e \u043f\u0456\u0441\u043b\u044f \u0434\u043e\u0432\u0433\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f", + "remote_double_button_short_press": "{subtype} \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u043e \u043f\u0456\u0441\u043b\u044f \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f" + } + }, + "options": { + "step": { + "init": { + "data": { + "allow_hue_groups": "\u0414\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0438 \u0433\u0440\u0443\u043f\u0438 Hue", + "allow_unreachable": "\u041f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u044f\u0442\u0438 \u0441\u0442\u0430\u043d \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/__init__.py b/homeassistant/components/huisbaasje/__init__.py new file mode 100644 index 00000000000..23dc3cb7eda --- /dev/null +++ b/homeassistant/components/huisbaasje/__init__.py @@ -0,0 +1,168 @@ +"""The Huisbaasje integration.""" +from datetime import timedelta +import logging + +import async_timeout +from huisbaasje import Huisbaasje, HuisbaasjeException + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + DATA_COORDINATOR, + DOMAIN, + FETCH_TIMEOUT, + POLLING_INTERVAL, + SENSOR_TYPE_RATE, + SENSOR_TYPE_THIS_DAY, + SENSOR_TYPE_THIS_MONTH, + SENSOR_TYPE_THIS_WEEK, + SENSOR_TYPE_THIS_YEAR, + SOURCE_TYPES, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Huisbaasje component.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): + """Set up Huisbaasje from a config entry.""" + # Create the Huisbaasje client + huisbaasje = Huisbaasje( + username=config_entry.data[CONF_USERNAME], + password=config_entry.data[CONF_PASSWORD], + source_types=SOURCE_TYPES, + request_timeout=FETCH_TIMEOUT, + ) + + # Attempt authentication. If this fails, an exception is thrown + try: + await huisbaasje.authenticate() + except HuisbaasjeException as exception: + _LOGGER.error("Authentication failed: %s", str(exception)) + return False + + async def async_update_data(): + return await async_update_huisbaasje(huisbaasje) + + # Create a coordinator for polling updates + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="sensor", + update_method=async_update_data, + update_interval=timedelta(seconds=POLLING_INTERVAL), + ) + + await coordinator.async_refresh() + + if not coordinator.last_update_success: + raise ConfigEntryNotReady + + # Load the client in the data of home assistant + hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = { + DATA_COORDINATOR: coordinator + } + + # Offload the loading of entities to the platform + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, "sensor") + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): + """Unload a config entry.""" + # Forward the unloading of the entry to the platform + unload_ok = await hass.config_entries.async_forward_entry_unload( + config_entry, "sensor" + ) + + # If successful, unload the Huisbaasje client + if unload_ok: + hass.data[DOMAIN].pop(config_entry.entry_id) + + return unload_ok + + +async def async_update_huisbaasje(huisbaasje): + """Update the data by performing a request to Huisbaasje.""" + try: + # Note: asyncio.TimeoutError and aiohttp.ClientError are already + # handled by the data update coordinator. + async with async_timeout.timeout(FETCH_TIMEOUT): + if not huisbaasje.is_authenticated(): + _LOGGER.warning("Huisbaasje is unauthenticated. Reauthenticating...") + await huisbaasje.authenticate() + + current_measurements = await huisbaasje.current_measurements() + + return { + source_type: { + SENSOR_TYPE_RATE: _get_measurement_rate( + current_measurements, source_type + ), + SENSOR_TYPE_THIS_DAY: _get_cumulative_value( + current_measurements, source_type, SENSOR_TYPE_THIS_DAY + ), + SENSOR_TYPE_THIS_WEEK: _get_cumulative_value( + current_measurements, source_type, SENSOR_TYPE_THIS_WEEK + ), + SENSOR_TYPE_THIS_MONTH: _get_cumulative_value( + current_measurements, source_type, SENSOR_TYPE_THIS_MONTH + ), + SENSOR_TYPE_THIS_YEAR: _get_cumulative_value( + current_measurements, source_type, SENSOR_TYPE_THIS_YEAR + ), + } + for source_type in SOURCE_TYPES + } + except HuisbaasjeException as exception: + raise UpdateFailed(f"Error communicating with API: {exception}") from exception + + +def _get_cumulative_value( + current_measurements: dict, + source_type: str, + period_type: str, +): + """ + Get the cumulative energy consumption for a certain period. + + :param current_measurements: The result from the Huisbaasje client + :param source_type: The source of energy (electricity or gas) + :param period_type: The period for which cumulative value should be given. + """ + if source_type in current_measurements.keys(): + if ( + period_type in current_measurements[source_type] + and current_measurements[source_type][period_type] is not None + ): + return current_measurements[source_type][period_type]["value"] + else: + _LOGGER.error( + "Source type %s not present in %s", source_type, current_measurements + ) + return None + + +def _get_measurement_rate(current_measurements: dict, source_type: str): + if source_type in current_measurements: + if ( + "measurement" in current_measurements[source_type] + and current_measurements[source_type]["measurement"] is not None + ): + return current_measurements[source_type]["measurement"]["rate"] + else: + _LOGGER.error( + "Source type %s not present in %s", source_type, current_measurements + ) + return None diff --git a/homeassistant/components/huisbaasje/config_flow.py b/homeassistant/components/huisbaasje/config_flow.py new file mode 100644 index 00000000000..59e4840529d --- /dev/null +++ b/homeassistant/components/huisbaasje/config_flow.py @@ -0,0 +1,84 @@ +"""Config flow for Huisbaasje integration.""" +import logging + +from huisbaasje import Huisbaasje, HuisbaasjeConnectionException, HuisbaasjeException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME +from homeassistant.data_entry_flow import AbortFlow + +from .const import DOMAIN # pylint: disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} +) + + +class HuisbaasjeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Huisbaasje.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + if user_input is None: + return await self._show_setup_form(user_input) + + errors = {} + + try: + user_id = await self._validate_input(user_input) + + _LOGGER.info("Input for Huisbaasje is valid!") + + # Set user id as unique id + await self.async_set_unique_id(user_id) + self._abort_if_unique_id_configured() + + # Create entry + return self.async_create_entry( + title=user_input[CONF_USERNAME], + data={ + CONF_ID: user_id, + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], + }, + ) + except HuisbaasjeConnectionException as exception: + _LOGGER.warning(exception) + errors["base"] = "connection_exception" + except HuisbaasjeException as exception: + _LOGGER.warning(exception) + errors["base"] = "invalid_auth" + except AbortFlow as exception: + raise exception + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + return await self._show_setup_form(user_input, errors) + + async def _show_setup_form(self, user_input, errors=None): + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors or {} + ) + + async def _validate_input(self, user_input): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + + username = user_input[CONF_USERNAME] + password = user_input[CONF_PASSWORD] + + huisbaasje = Huisbaasje(username, password) + + # Attempt authentication. If this fails, an HuisbaasjeException will be thrown + await huisbaasje.authenticate() + + return huisbaasje.get_user_id() diff --git a/homeassistant/components/huisbaasje/const.py b/homeassistant/components/huisbaasje/const.py new file mode 100644 index 00000000000..07ad84567e5 --- /dev/null +++ b/homeassistant/components/huisbaasje/const.py @@ -0,0 +1,142 @@ +"""Constants for the Huisbaasje integration.""" +from huisbaasje.const import ( + SOURCE_TYPE_ELECTRICITY, + SOURCE_TYPE_ELECTRICITY_IN, + SOURCE_TYPE_ELECTRICITY_IN_LOW, + SOURCE_TYPE_ELECTRICITY_OUT, + SOURCE_TYPE_ELECTRICITY_OUT_LOW, + SOURCE_TYPE_GAS, +) + +from homeassistant.const import ( + DEVICE_CLASS_POWER, + ENERGY_KILO_WATT_HOUR, + TIME_HOURS, + VOLUME_CUBIC_METERS, +) + +DATA_COORDINATOR = "coordinator" + +DOMAIN = "huisbaasje" + +FLOW_CUBIC_METERS_PER_HOUR = f"{VOLUME_CUBIC_METERS}/{TIME_HOURS}" + +"""Interval in seconds between polls to huisbaasje.""" +POLLING_INTERVAL = 20 + +"""Timeout for fetching sensor data""" +FETCH_TIMEOUT = 10 + +SENSOR_TYPE_RATE = "rate" +SENSOR_TYPE_THIS_DAY = "thisDay" +SENSOR_TYPE_THIS_WEEK = "thisWeek" +SENSOR_TYPE_THIS_MONTH = "thisMonth" +SENSOR_TYPE_THIS_YEAR = "thisYear" + +SOURCE_TYPES = [ + SOURCE_TYPE_ELECTRICITY, + SOURCE_TYPE_ELECTRICITY_IN, + SOURCE_TYPE_ELECTRICITY_IN_LOW, + SOURCE_TYPE_ELECTRICITY_OUT, + SOURCE_TYPE_ELECTRICITY_OUT_LOW, + SOURCE_TYPE_GAS, +] + +SENSORS_INFO = [ + { + "name": "Huisbaasje Current Power", + "device_class": DEVICE_CLASS_POWER, + "source_type": SOURCE_TYPE_ELECTRICITY, + }, + { + "name": "Huisbaasje Current Power In", + "device_class": DEVICE_CLASS_POWER, + "source_type": SOURCE_TYPE_ELECTRICITY_IN, + }, + { + "name": "Huisbaasje Current Power In Low", + "device_class": DEVICE_CLASS_POWER, + "source_type": SOURCE_TYPE_ELECTRICITY_IN_LOW, + }, + { + "name": "Huisbaasje Current Power Out", + "device_class": DEVICE_CLASS_POWER, + "source_type": SOURCE_TYPE_ELECTRICITY_OUT, + }, + { + "name": "Huisbaasje Current Power Out Low", + "device_class": DEVICE_CLASS_POWER, + "source_type": SOURCE_TYPE_ELECTRICITY_OUT_LOW, + }, + { + "name": "Huisbaasje Energy Today", + "unit_of_measurement": ENERGY_KILO_WATT_HOUR, + "source_type": SOURCE_TYPE_ELECTRICITY, + "sensor_type": SENSOR_TYPE_THIS_DAY, + "icon": "mdi:counter", + "precision": 1, + }, + { + "name": "Huisbaasje Energy This Week", + "unit_of_measurement": ENERGY_KILO_WATT_HOUR, + "source_type": SOURCE_TYPE_ELECTRICITY, + "sensor_type": SENSOR_TYPE_THIS_WEEK, + "icon": "mdi:counter", + "precision": 1, + }, + { + "name": "Huisbaasje Energy This Month", + "unit_of_measurement": ENERGY_KILO_WATT_HOUR, + "source_type": SOURCE_TYPE_ELECTRICITY, + "sensor_type": SENSOR_TYPE_THIS_MONTH, + "icon": "mdi:counter", + "precision": 1, + }, + { + "name": "Huisbaasje Energy This Year", + "unit_of_measurement": ENERGY_KILO_WATT_HOUR, + "source_type": SOURCE_TYPE_ELECTRICITY, + "sensor_type": SENSOR_TYPE_THIS_YEAR, + "icon": "mdi:counter", + "precision": 1, + }, + { + "name": "Huisbaasje Current Gas", + "unit_of_measurement": FLOW_CUBIC_METERS_PER_HOUR, + "source_type": SOURCE_TYPE_GAS, + "icon": "mdi:fire", + "precision": 1, + }, + { + "name": "Huisbaasje Gas Today", + "unit_of_measurement": VOLUME_CUBIC_METERS, + "source_type": SOURCE_TYPE_GAS, + "sensor_type": SENSOR_TYPE_THIS_DAY, + "icon": "mdi:counter", + "precision": 1, + }, + { + "name": "Huisbaasje Gas This Week", + "unit_of_measurement": VOLUME_CUBIC_METERS, + "source_type": SOURCE_TYPE_GAS, + "sensor_type": SENSOR_TYPE_THIS_WEEK, + "icon": "mdi:counter", + "precision": 1, + }, + { + "name": "Huisbaasje Gas This Month", + "unit_of_measurement": VOLUME_CUBIC_METERS, + "source_type": SOURCE_TYPE_GAS, + "sensor_type": SENSOR_TYPE_THIS_MONTH, + "icon": "mdi:counter", + "precision": 1, + }, + { + "name": "Huisbaasje Gas This Year", + "unit_of_measurement": VOLUME_CUBIC_METERS, + "source_type": SOURCE_TYPE_GAS, + "sensor_type": SENSOR_TYPE_THIS_YEAR, + "icon": "mdi:counter", + "precision": 1, + }, +] diff --git a/homeassistant/components/huisbaasje/manifest.json b/homeassistant/components/huisbaasje/manifest.json new file mode 100644 index 00000000000..975adb52a22 --- /dev/null +++ b/homeassistant/components/huisbaasje/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "huisbaasje", + "name": "Huisbaasje", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/huisbaasje", + "requirements": [ + "huisbaasje-client==0.1.0" + ], + "codeowners": ["@denniss17"] +} diff --git a/homeassistant/components/huisbaasje/sensor.py b/homeassistant/components/huisbaasje/sensor.py new file mode 100644 index 00000000000..e84052fe029 --- /dev/null +++ b/homeassistant/components/huisbaasje/sensor.py @@ -0,0 +1,95 @@ +"""Platform for sensor integration.""" +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_ID, POWER_WATT +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) + +from .const import DATA_COORDINATOR, DOMAIN, SENSOR_TYPE_RATE, SENSORS_INFO + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities +): + """Set up the sensor platform.""" + coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR] + user_id = config_entry.data[CONF_ID] + + async_add_entities( + HuisbaasjeSensor(coordinator, user_id=user_id, **sensor_info) + for sensor_info in SENSORS_INFO + ) + + +class HuisbaasjeSensor(CoordinatorEntity): + """Defines a Huisbaasje sensor.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator, + user_id: str, + name: str, + source_type: str, + device_class: str = None, + sensor_type: str = SENSOR_TYPE_RATE, + unit_of_measurement: str = POWER_WATT, + icon: str = "mdi:lightning-bolt", + precision: int = 0, + ): + """Initialize the sensor.""" + super().__init__(coordinator) + self._user_id = user_id + self._name = name + self._device_class = device_class + self._unit_of_measurement = unit_of_measurement + self._source_type = source_type + self._sensor_type = sensor_type + self._icon = icon + self._precision = precision + + @property + def unique_id(self) -> str: + """Return an unique id for the sensor.""" + return f"{DOMAIN}_{self._user_id}_{self._source_type}_{self._sensor_type}" + + @property + def name(self) -> str: + """Return the name of the sensor.""" + return self._name + + @property + def device_class(self) -> str: + """Return the device class of the sensor.""" + return self._device_class + + @property + def icon(self) -> str: + """Return the icon to use for the sensor.""" + return self._icon + + @property + def state(self): + """Return the state of the sensor.""" + if self.coordinator.data[self._source_type][self._sensor_type] is not None: + return round( + self.coordinator.data[self._source_type][self._sensor_type], + self._precision, + ) + return None + + @property + def unit_of_measurement(self) -> str: + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def available(self) -> bool: + """Return if entity is available.""" + return ( + super().available + and self.coordinator.data + and self._source_type in self.coordinator.data + and self.coordinator.data[self._source_type] + ) diff --git a/homeassistant/components/huisbaasje/strings.json b/homeassistant/components/huisbaasje/strings.json new file mode 100644 index 00000000000..f126ac0afff --- /dev/null +++ b/homeassistant/components/huisbaasje/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "user": { + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unauthenticated_exception": "[%key:common::config_flow::error::invalid_auth%]", + "connection_exception": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/huisbaasje/translations/ca.json b/homeassistant/components/huisbaasje/translations/ca.json new file mode 100644 index 00000000000..99d99d4340f --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "connection_exception": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unauthenticated_exception": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/cs.json b/homeassistant/components/huisbaasje/translations/cs.json new file mode 100644 index 00000000000..07a1d29330b --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/cs.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "connection_exception": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unauthenticated_exception": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "password": "Heslo", + "username": "U\u017eivatelsk\u00e9 jm\u00e9no" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/en.json b/homeassistant/components/huisbaasje/translations/en.json new file mode 100644 index 00000000000..16832be30e7 --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "connection_exception": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unauthenticated_exception": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/et.json b/homeassistant/components/huisbaasje/translations/et.json new file mode 100644 index 00000000000..d079bf2a0c7 --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/et.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "connection_exception": "\u00dchendamine nurjus", + "invalid_auth": "Vigane autentimine", + "unauthenticated_exception": "Vigane autentimine", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "password": "Salas\u00f5na", + "username": "Kasutajanimi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/it.json b/homeassistant/components/huisbaasje/translations/it.json new file mode 100644 index 00000000000..0171bdcd9f2 --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "connection_exception": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unauthenticated_exception": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/no.json b/homeassistant/components/huisbaasje/translations/no.json new file mode 100644 index 00000000000..81351599c16 --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "connection_exception": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unauthenticated_exception": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/pl.json b/homeassistant/components/huisbaasje/translations/pl.json new file mode 100644 index 00000000000..ab38d61de0b --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "connection_exception": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unauthenticated_exception": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/ru.json b/homeassistant/components/huisbaasje/translations/ru.json new file mode 100644 index 00000000000..ada9aed539a --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "connection_exception": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unauthenticated_exception": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/tr.json b/homeassistant/components/huisbaasje/translations/tr.json new file mode 100644 index 00000000000..fa5bd311286 --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "connection_exception": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unauthenticated_exception": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen Hata" + }, + "step": { + "user": { + "data": { + "password": "\u015eifre", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huisbaasje/translations/zh-Hant.json b/homeassistant/components/huisbaasje/translations/zh-Hant.json new file mode 100644 index 00000000000..bb120ab60dd --- /dev/null +++ b/homeassistant/components/huisbaasje/translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "connection_exception": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unauthenticated_exception": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/tr.json b/homeassistant/components/humidifier/translations/tr.json new file mode 100644 index 00000000000..7bcdbc46a0b --- /dev/null +++ b/homeassistant/components/humidifier/translations/tr.json @@ -0,0 +1,25 @@ +{ + "device_automation": { + "action_type": { + "set_mode": "{entity_name} \u00fczerindeki mod de\u011fi\u015ftirme", + "turn_on": "{entity_name} a\u00e7\u0131n" + }, + "condition_type": { + "is_mode": "{entity_name} belirli bir moda ayarland\u0131", + "is_off": "{entity_name} kapal\u0131", + "is_on": "{entity_name} a\u00e7\u0131k" + }, + "trigger_type": { + "target_humidity_changed": "{entity_name} hedef nem de\u011fi\u015fti", + "turned_off": "{entity_name} kapat\u0131ld\u0131", + "turned_on": "{entity_name} a\u00e7\u0131ld\u0131" + } + }, + "state": { + "_": { + "off": "Kapal\u0131", + "on": "A\u00e7\u0131k" + } + }, + "title": "Nemlendirici" +} \ No newline at end of file diff --git a/homeassistant/components/humidifier/translations/uk.json b/homeassistant/components/humidifier/translations/uk.json index 4081c4e13fc..484f014bd92 100644 --- a/homeassistant/components/humidifier/translations/uk.json +++ b/homeassistant/components/humidifier/translations/uk.json @@ -1,8 +1,28 @@ { "device_automation": { + "action_type": { + "set_humidity": "{entity_name}: \u0437\u0430\u0434\u0430\u0442\u0438 \u0432\u043e\u043b\u043e\u0433\u0456\u0441\u0442\u044c", + "set_mode": "{entity_name}: \u0437\u043c\u0456\u043d\u0438\u0442\u0438 \u0440\u0435\u0436\u0438\u043c", + "toggle": "{entity_name}: \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u0438", + "turn_off": "{entity_name}: \u0432\u0438\u043c\u043a\u043d\u0443\u0442\u0438", + "turn_on": "{entity_name}: \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438" + }, + "condition_type": { + "is_mode": "{entity_name} \u0437\u043d\u0430\u0445\u043e\u0434\u0438\u0442\u044c\u0441\u044f \u0432 \u0437\u0430\u0434\u0430\u043d\u043e\u043c\u0443 \u0440\u0435\u0436\u0438\u043c\u0456 \u0440\u043e\u0431\u043e\u0442\u0438", + "is_off": "{entity_name} \u0443 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_on": "{entity_name} \u0443 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456" + }, "trigger_type": { + "target_humidity_changed": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0437\u0430\u0434\u0430\u043d\u043e\u0457 \u0432\u043e\u043b\u043e\u0433\u043e\u0441\u0442\u0456", "turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e", - "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u043e" + "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e" } - } + }, + "state": { + "_": { + "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e", + "on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e" + } + }, + "title": "\u0417\u0432\u043e\u043b\u043e\u0436\u0443\u0432\u0430\u0447" } \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/__init__.py b/homeassistant/components/hunterdouglas_powerview/__init__.py index c87097dc7af..7555146ba8e 100644 --- a/homeassistant/components/hunterdouglas_powerview/__init__.py +++ b/homeassistant/components/hunterdouglas_powerview/__init__.py @@ -11,9 +11,8 @@ from aiopvapi.scenes import Scenes from aiopvapi.shades import Shades from aiopvapi.userdata import UserData import async_timeout -import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady @@ -58,25 +57,7 @@ from .const import ( PARALLEL_UPDATES = 1 - -DEVICE_SCHEMA = vol.Schema( - {DOMAIN: vol.Schema({vol.Required(CONF_HOST): cv.string})}, extra=vol.ALLOW_EXTRA -) - - -def _has_all_unique_hosts(value): - """Validate that each hub configured has a unique host.""" - hosts = [device[CONF_HOST] for device in value] - schema = vol.Schema(vol.Unique()) - schema(hosts) - return value - - -CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.All(cv.ensure_list, [DEVICE_SCHEMA], _has_all_unique_hosts)}, - extra=vol.ALLOW_EXTRA, -) - +CONFIG_SCHEMA = cv.deprecated(DOMAIN) PLATFORMS = ["cover", "scene", "sensor"] _LOGGER = logging.getLogger(__name__) @@ -85,17 +66,6 @@ _LOGGER = logging.getLogger(__name__) async def async_setup(hass: HomeAssistant, hass_config: dict): """Set up the Hunter Douglas PowerView component.""" hass.data.setdefault(DOMAIN, {}) - - if DOMAIN not in hass_config: - return True - - for conf in hass_config[DOMAIN]: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=conf - ) - ) - return True diff --git a/homeassistant/components/hunterdouglas_powerview/config_flow.py b/homeassistant/components/hunterdouglas_powerview/config_flow.py index 2bbdcfea3a6..34ae94e4b88 100644 --- a/homeassistant/components/hunterdouglas_powerview/config_flow.py +++ b/homeassistant/components/hunterdouglas_powerview/config_flow.py @@ -79,10 +79,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_import(self, user_input=None): - """Handle the initial step.""" - return await self.async_step_user(user_input) - async def async_step_homekit(self, homekit_info): """Handle HomeKit discovery.""" @@ -126,7 +122,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def _host_already_configured(self, host): """See if we already have a hub with the host address configured.""" existing_hosts = { - entry.data[CONF_HOST] + entry.data.get(CONF_HOST) for entry in self._async_current_entries() if CONF_HOST in entry.data } diff --git a/homeassistant/components/hunterdouglas_powerview/translations/tr.json b/homeassistant/components/hunterdouglas_powerview/translations/tr.json new file mode 100644 index 00000000000..01b0359789e --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "\u0130p Adresi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hunterdouglas_powerview/translations/uk.json b/homeassistant/components/hunterdouglas_powerview/translations/uk.json new file mode 100644 index 00000000000..959fcff12b0 --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/uk.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "link": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name} ({host})?", + "title": "Hunter Douglas PowerView" + }, + "user": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430" + }, + "title": "Hunter Douglas PowerView" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hvv_departures/translations/tr.json b/homeassistant/components/hvv_departures/translations/tr.json new file mode 100644 index 00000000000..74fc593062b --- /dev/null +++ b/homeassistant/components/hvv_departures/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hvv_departures/translations/uk.json b/homeassistant/components/hvv_departures/translations/uk.json new file mode 100644 index 00000000000..364d351a99c --- /dev/null +++ b/homeassistant/components/hvv_departures/translations/uk.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "no_results": "\u041d\u0435\u043c\u0430\u0454 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0456\u0432. \u0421\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437 \u0456\u043d\u0448\u043e\u044e \u0441\u0442\u0430\u043d\u0446\u0456\u0454\u044e / \u0430\u0434\u0440\u0435\u0441\u043e\u044e." + }, + "step": { + "station": { + "data": { + "station": "\u0421\u0442\u0430\u043d\u0446\u0456\u044f / \u0410\u0434\u0440\u0435\u0441\u0430" + }, + "title": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0441\u0442\u0430\u043d\u0446\u0456\u044e / \u0430\u0434\u0440\u0435\u0441\u0443" + }, + "station_select": { + "data": { + "station": "\u0421\u0442\u0430\u043d\u0446\u0456\u044f / \u0410\u0434\u0440\u0435\u0441\u0430" + }, + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0442\u0430\u043d\u0446\u0456\u044e / \u0430\u0434\u0440\u0435\u0441\u0443" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e API HVV" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "filter": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043b\u0456\u043d\u0456\u0457", + "offset": "\u0417\u043c\u0456\u0449\u0435\u043d\u043d\u044f (\u0432 \u0445\u0432\u0438\u043b\u0438\u043d\u0430\u0445)", + "real_time": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0434\u0430\u043d\u0456 \u0432 \u0440\u0435\u0436\u0438\u043c\u0456 \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0447\u0430\u0441\u0443" + }, + "description": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0430 \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043d\u044f", + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hyperion/__init__.py b/homeassistant/components/hyperion/__init__.py index 05494d14869..aeac922826d 100644 --- a/homeassistant/components/hyperion/__init__.py +++ b/homeassistant/components/hyperion/__init__.py @@ -2,29 +2,40 @@ import asyncio import logging -from typing import Any, Optional +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, cast from hyperion import client, const as hyperion_const from pkg_resources import parse_version from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SOURCE, CONF_TOKEN -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity_registry import ( + async_entries_for_config_entry, + async_get_registry, +) from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .const import ( + CONF_INSTANCE_CLIENTS, CONF_ON_UNLOAD, CONF_ROOT_CLIENT, + DEFAULT_NAME, DOMAIN, HYPERION_RELEASES_URL, HYPERION_VERSION_WARN_CUTOFF, - SIGNAL_INSTANCES_UPDATED, + SIGNAL_INSTANCE_ADD, + SIGNAL_INSTANCE_REMOVE, ) -PLATFORMS = [LIGHT_DOMAIN] +PLATFORMS = [LIGHT_DOMAIN, SWITCH_DOMAIN] _LOGGER = logging.getLogger(__name__) @@ -59,6 +70,17 @@ def get_hyperion_unique_id(server_id: str, instance: int, name: str) -> str: return f"{server_id}_{instance}_{name}" +def split_hyperion_unique_id(unique_id: str) -> Optional[Tuple[str, int, str]]: + """Split a unique_id into a (server_id, instance, type) tuple.""" + data = tuple(unique_id.split("_", 2)) + if len(data) != 3: + return None + try: + return (data[0], int(data[1]), data[2]) + except ValueError: + return None + + def create_hyperion_client( *args: Any, **kwargs: Any, @@ -96,6 +118,31 @@ async def _create_reauth_flow( ) +@callback +def listen_for_instance_updates( + hass: HomeAssistant, + config_entry: ConfigEntry, + add_func: Callable, + remove_func: Callable, +) -> None: + """Listen for instance additions/removals.""" + + hass.data[DOMAIN][config_entry.entry_id][CONF_ON_UNLOAD].extend( + [ + async_dispatcher_connect( + hass, + SIGNAL_INSTANCE_ADD.format(config_entry.entry_id), + add_func, + ), + async_dispatcher_connect( + hass, + SIGNAL_INSTANCE_REMOVE.format(config_entry.entry_id), + remove_func, + ), + ] + ) + + async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Set up Hyperion from a config entry.""" host = config_entry.data[CONF_HOST] @@ -133,11 +180,13 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ) and token is None ): + await hyperion_client.async_client_disconnect() await _create_reauth_flow(hass, config_entry) return False # Client login doesn't work? => Reauth. if not await hyperion_client.async_client_login(): + await hyperion_client.async_client_disconnect() await _create_reauth_flow(hass, config_entry) return False @@ -146,25 +195,89 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b not await hyperion_client.async_client_switch_instance() or not client.ServerInfoResponseOK(await hyperion_client.async_get_serverinfo()) ): + await hyperion_client.async_client_disconnect() raise ConfigEntryNotReady + # We need 1 root client (to manage instances being removed/added) and then 1 client + # per Hyperion server instance which is shared for all entities associated with + # that instance. + hass.data[DOMAIN][config_entry.entry_id] = { + CONF_ROOT_CLIENT: hyperion_client, + CONF_INSTANCE_CLIENTS: {}, + CONF_ON_UNLOAD: [], + } + + async def async_instances_to_clients(response: Dict[str, Any]) -> None: + """Convert instances to Hyperion clients.""" + if not response or hyperion_const.KEY_DATA not in response: + return + await async_instances_to_clients_raw(response[hyperion_const.KEY_DATA]) + + async def async_instances_to_clients_raw(instances: List[Dict[str, Any]]) -> None: + """Convert instances to Hyperion clients.""" + registry = await async_get_registry(hass) + running_instances: Set[int] = set() + stopped_instances: Set[int] = set() + existing_instances = hass.data[DOMAIN][config_entry.entry_id][ + CONF_INSTANCE_CLIENTS + ] + server_id = cast(str, config_entry.unique_id) + + # In practice, an instance can be in 3 states as seen by this function: + # + # * Exists, and is running: Should be present in HASS/registry. + # * Exists, but is not running: Cannot add it yet, but entity may have be + # registered from a previous time it was running. + # * No longer exists at all: Should not be present in HASS/registry. + + # Add instances that are missing. + for instance in instances: + instance_num = instance.get(hyperion_const.KEY_INSTANCE) + if instance_num is None: + continue + if not instance.get(hyperion_const.KEY_RUNNING, False): + stopped_instances.add(instance_num) + continue + running_instances.add(instance_num) + if instance_num in existing_instances: + continue + hyperion_client = await async_create_connect_hyperion_client( + host, port, instance=instance_num, token=token + ) + if not hyperion_client: + continue + existing_instances[instance_num] = hyperion_client + instance_name = instance.get(hyperion_const.KEY_FRIENDLY_NAME, DEFAULT_NAME) + async_dispatcher_send( + hass, + SIGNAL_INSTANCE_ADD.format(config_entry.entry_id), + instance_num, + instance_name, + ) + + # Remove entities that are are not running instances on Hyperion. + for instance_num in set(existing_instances) - running_instances: + del existing_instances[instance_num] + async_dispatcher_send( + hass, SIGNAL_INSTANCE_REMOVE.format(config_entry.entry_id), instance_num + ) + + # Deregister entities that belong to removed instances. + for entry in async_entries_for_config_entry(registry, config_entry.entry_id): + data = split_hyperion_unique_id(entry.unique_id) + if not data: + continue + if data[0] == server_id and ( + data[1] not in running_instances and data[1] not in stopped_instances + ): + registry.async_remove(entry.entity_id) + hyperion_client.set_callbacks( { - f"{hyperion_const.KEY_INSTANCE}-{hyperion_const.KEY_UPDATE}": lambda json: ( - async_dispatcher_send( - hass, - SIGNAL_INSTANCES_UPDATED.format(config_entry.entry_id), - json, - ) - ) + f"{hyperion_const.KEY_INSTANCE}-{hyperion_const.KEY_UPDATE}": async_instances_to_clients, } ) - hass.data[DOMAIN][config_entry.entry_id] = { - CONF_ROOT_CLIENT: hyperion_client, - CONF_ON_UNLOAD: [], - } - # Must only listen for option updates after the setup is complete, as otherwise # the YAML->ConfigEntry migration code triggers an options update, which causes a # reload -- which clashes with the initial load (causing entity_id / unique_id @@ -176,6 +289,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b for component in PLATFORMS ] ) + assert hyperion_client + await async_instances_to_clients_raw(hyperion_client.instances) hass.data[DOMAIN][config_entry.entry_id][CONF_ON_UNLOAD].append( config_entry.add_update_listener(_async_entry_updated) ) @@ -207,6 +322,18 @@ async def async_unload_entry( config_data = hass.data[DOMAIN].pop(config_entry.entry_id) for func in config_data[CONF_ON_UNLOAD]: func() + + # Disconnect the shared instance clients. + await asyncio.gather( + *[ + config_data[CONF_INSTANCE_CLIENTS][ + instance_num + ].async_client_disconnect() + for instance_num in config_data[CONF_INSTANCE_CLIENTS] + ] + ) + + # Disconnect the root client. root_client = config_data[CONF_ROOT_CLIENT] await root_client.async_client_disconnect() return unload_ok diff --git a/homeassistant/components/hyperion/const.py b/homeassistant/components/hyperion/const.py index 2bb9ec241e5..64c2f20052b 100644 --- a/homeassistant/components/hyperion/const.py +++ b/homeassistant/components/hyperion/const.py @@ -1,8 +1,33 @@ """Constants for Hyperion integration.""" +from hyperion.const import ( + KEY_COMPONENTID_ALL, + KEY_COMPONENTID_BLACKBORDER, + KEY_COMPONENTID_BOBLIGHTSERVER, + KEY_COMPONENTID_FORWARDER, + KEY_COMPONENTID_GRABBER, + KEY_COMPONENTID_LEDDEVICE, + KEY_COMPONENTID_SMOOTHING, + KEY_COMPONENTID_V4L, +) + +# Maps between Hyperion API component names to Hyperion UI names. This allows Home +# Assistant to use names that match what Hyperion users may expect from the Hyperion UI. +COMPONENT_TO_NAME = { + KEY_COMPONENTID_ALL: "All", + KEY_COMPONENTID_SMOOTHING: "Smoothing", + KEY_COMPONENTID_BLACKBORDER: "Blackbar Detection", + KEY_COMPONENTID_FORWARDER: "Forwarder", + KEY_COMPONENTID_BOBLIGHTSERVER: "Boblight Server", + KEY_COMPONENTID_GRABBER: "Platform Capture", + KEY_COMPONENTID_LEDDEVICE: "LED Device", + KEY_COMPONENTID_V4L: "USB Capture", +} + CONF_AUTH_ID = "auth_id" CONF_CREATE_TOKEN = "create_token" CONF_INSTANCE = "instance" +CONF_INSTANCE_CLIENTS = "INSTANCE_CLIENTS" CONF_ON_UNLOAD = "ON_UNLOAD" CONF_PRIORITY = "priority" CONF_ROOT_CLIENT = "ROOT_CLIENT" @@ -16,7 +41,14 @@ DOMAIN = "hyperion" HYPERION_RELEASES_URL = "https://github.com/hyperion-project/hyperion.ng/releases" HYPERION_VERSION_WARN_CUTOFF = "2.0.0-alpha.9" -SIGNAL_INSTANCES_UPDATED = f"{DOMAIN}_instances_updated_signal." "{}" -SIGNAL_INSTANCE_REMOVED = f"{DOMAIN}_instance_removed_signal." "{}" +NAME_SUFFIX_HYPERION_LIGHT = "" +NAME_SUFFIX_HYPERION_PRIORITY_LIGHT = "Priority" +NAME_SUFFIX_HYPERION_COMPONENT_SWITCH = "Component" + +SIGNAL_INSTANCE_ADD = f"{DOMAIN}_instance_add_signal." "{}" +SIGNAL_INSTANCE_REMOVE = f"{DOMAIN}_instance_remove_signal." "{}" +SIGNAL_ENTITY_REMOVE = f"{DOMAIN}_entity_remove_signal." "{}" TYPE_HYPERION_LIGHT = "hyperion_light" +TYPE_HYPERION_PRIORITY_LIGHT = "hyperion_priority_light" +TYPE_HYPERION_COMPONENT_SWITCH_BASE = "hyperion_component_switch" diff --git a/homeassistant/components/hyperion/light.py b/homeassistant/components/hyperion/light.py index e2989cb973b..a329ee5c20e 100644 --- a/homeassistant/components/hyperion/light.py +++ b/homeassistant/components/hyperion/light.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging import re from types import MappingProxyType -from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Tuple, cast +from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple from hyperion import client, const import voluptuous as vol @@ -22,7 +22,8 @@ from homeassistant.components.light import ( LightEntity, ) from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_TOKEN +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT +from homeassistant.core import callback from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( @@ -37,21 +38,28 @@ from homeassistant.helpers.typing import ( ) import homeassistant.util.color as color_util -from . import async_create_connect_hyperion_client, get_hyperion_unique_id +from . import ( + create_hyperion_client, + get_hyperion_unique_id, + listen_for_instance_updates, +) from .const import ( - CONF_ON_UNLOAD, + CONF_INSTANCE_CLIENTS, CONF_PRIORITY, - CONF_ROOT_CLIENT, DEFAULT_ORIGIN, DEFAULT_PRIORITY, DOMAIN, - SIGNAL_INSTANCE_REMOVED, - SIGNAL_INSTANCES_UPDATED, + NAME_SUFFIX_HYPERION_LIGHT, + NAME_SUFFIX_HYPERION_PRIORITY_LIGHT, + SIGNAL_ENTITY_REMOVE, TYPE_HYPERION_LIGHT, + TYPE_HYPERION_PRIORITY_LIGHT, ) _LOGGER = logging.getLogger(__name__) +COLOR_BLACK = color_util.COLORS["black"] + CONF_DEFAULT_COLOR = "default_color" CONF_HDMI_PRIORITY = "hdmi_priority" CONF_EFFECT_LIST = "effect_list" @@ -132,12 +140,12 @@ async def async_setup_platform( # First, connect to the server and get the server id (which will be unique_id on a config_entry # if there is one). - hyperion_client = await async_create_connect_hyperion_client(host, port) - if not hyperion_client: - raise PlatformNotReady - hyperion_id = await hyperion_client.async_sysinfo_id() - if not hyperion_id: - raise PlatformNotReady + async with create_hyperion_client(host, port) as hyperion_client: + if not hyperion_client: + raise PlatformNotReady + hyperion_id = await hyperion_client.async_sysinfo_id() + if not hyperion_id: + raise PlatformNotReady future_unique_id = get_hyperion_unique_id( hyperion_id, instance, TYPE_HYPERION_LIGHT @@ -219,83 +227,53 @@ async def async_setup_entry( hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable ) -> bool: """Set up a Hyperion platform from config entry.""" - host = config_entry.data[CONF_HOST] - port = config_entry.data[CONF_PORT] - token = config_entry.data.get(CONF_TOKEN) - async def async_instances_to_entities(response: Dict[str, Any]) -> None: - if not response or const.KEY_DATA not in response: - return - await async_instances_to_entities_raw(response[const.KEY_DATA]) + entry_data = hass.data[DOMAIN][config_entry.entry_id] + server_id = config_entry.unique_id - async def async_instances_to_entities_raw(instances: List[Dict[str, Any]]) -> None: - registry = await async_get_registry(hass) - entities_to_add: List[HyperionLight] = [] - desired_unique_ids: Set[str] = set() - server_id = cast(str, config_entry.unique_id) - - # In practice, an instance can be in 3 states as seen by this function: - # - # * Exists, and is running: Add it to hass. - # * Exists, but is not running: Cannot add yet, but should not delete it either. - # It will show up as "unavailable". - # * No longer exists: Delete it from hass. - - # Add instances that are missing. - for instance in instances: - instance_id = instance.get(const.KEY_INSTANCE) - if instance_id is None or not instance.get(const.KEY_RUNNING, False): - continue - unique_id = get_hyperion_unique_id( - server_id, instance_id, TYPE_HYPERION_LIGHT - ) - desired_unique_ids.add(unique_id) - if unique_id in current_entities: - continue - hyperion_client = await async_create_connect_hyperion_client( - host, port, instance=instance_id, token=token - ) - if not hyperion_client: - continue - current_entities.add(unique_id) - entities_to_add.append( + @callback + def instance_add(instance_num: int, instance_name: str) -> None: + """Add entities for a new Hyperion instance.""" + assert server_id + async_add_entities( + [ HyperionLight( - unique_id, - instance.get(const.KEY_FRIENDLY_NAME, DEFAULT_NAME), + get_hyperion_unique_id( + server_id, instance_num, TYPE_HYPERION_LIGHT + ), + f"{instance_name} {NAME_SUFFIX_HYPERION_LIGHT}", config_entry.options, - hyperion_client, - ) + entry_data[CONF_INSTANCE_CLIENTS][instance_num], + ), + HyperionPriorityLight( + get_hyperion_unique_id( + server_id, instance_num, TYPE_HYPERION_PRIORITY_LIGHT + ), + f"{instance_name} {NAME_SUFFIX_HYPERION_PRIORITY_LIGHT}", + config_entry.options, + entry_data[CONF_INSTANCE_CLIENTS][instance_num], + ), + ] + ) + + @callback + def instance_remove(instance_num: int) -> None: + """Remove entities for an old Hyperion instance.""" + assert server_id + for light_type in LIGHT_TYPES: + async_dispatcher_send( + hass, + SIGNAL_ENTITY_REMOVE.format( + get_hyperion_unique_id(server_id, instance_num, light_type) + ), ) - # Delete instances that are no longer present on this server. - for unique_id in current_entities - desired_unique_ids: - current_entities.remove(unique_id) - async_dispatcher_send(hass, SIGNAL_INSTANCE_REMOVED.format(unique_id)) - entity_id = registry.async_get_entity_id(LIGHT_DOMAIN, DOMAIN, unique_id) - if entity_id: - registry.async_remove(entity_id) - - async_add_entities(entities_to_add) - - # Readability note: This variable is kept alive in the context of the callback to - # async_instances_to_entities below. - current_entities: Set[str] = set() - - await async_instances_to_entities_raw( - hass.data[DOMAIN][config_entry.entry_id][CONF_ROOT_CLIENT].instances, - ) - hass.data[DOMAIN][config_entry.entry_id][CONF_ON_UNLOAD].append( - async_dispatcher_connect( - hass, - SIGNAL_INSTANCES_UPDATED.format(config_entry.entry_id), - async_instances_to_entities, - ) - ) + listen_for_instance_updates(hass, config_entry, instance_add, instance_remove) return True -class HyperionLight(LightEntity): - """Representation of a Hyperion remote.""" +class HyperionBaseLight(LightEntity): + """A Hyperion light base class.""" def __init__( self, @@ -314,9 +292,24 @@ class HyperionLight(LightEntity): self._brightness: int = 255 self._rgb_color: Sequence[int] = DEFAULT_COLOR self._effect: str = KEY_EFFECT_SOLID - self._icon: str = ICON_LIGHTBULB - self._effect_list: List[str] = [] + self._static_effect_list: List[str] = [KEY_EFFECT_SOLID] + if self._support_external_effects: + self._static_effect_list += list(const.KEY_COMPONENTID_EXTERNAL_SOURCES) + self._effect_list: List[str] = self._static_effect_list[:] + + self._client_callbacks = { + f"{const.KEY_ADJUSTMENT}-{const.KEY_UPDATE}": self._update_adjustment, + f"{const.KEY_COMPONENTS}-{const.KEY_UPDATE}": self._update_components, + f"{const.KEY_EFFECTS}-{const.KEY_UPDATE}": self._update_effect_list, + f"{const.KEY_PRIORITIES}-{const.KEY_UPDATE}": self._update_priorities, + f"{const.KEY_CLIENT}-{const.KEY_UPDATE}": self._update_client, + } + + @property + def entity_registry_enabled_default(self) -> bool: + """Whether or not the entity is enabled by default.""" + return True @property def should_poll(self) -> bool: @@ -338,15 +331,15 @@ class HyperionLight(LightEntity): """Return last color value set.""" return color_util.color_RGB_to_hs(*self._rgb_color) - @property - def is_on(self) -> bool: - """Return true if not black.""" - return bool(self._client.is_on()) and self._client.visible_priority is not None - @property def icon(self) -> str: """Return state specific icon.""" - return self._icon + if self.is_on: + if self.effect in const.KEY_COMPONENTID_EXTERNAL_SOURCES: + return ICON_EXTERNAL_SOURCE + if self.effect != KEY_EFFECT_SOLID: + return ICON_EFFECT + return ICON_LIGHTBULB @property def effect(self) -> str: @@ -356,11 +349,7 @@ class HyperionLight(LightEntity): @property def effect_list(self) -> List[str]: """Return the list of supported effects.""" - return ( - self._effect_list - + list(const.KEY_COMPONENTID_EXTERNAL_SOURCES) - + [KEY_EFFECT_SOLID] - ) + return self._effect_list @property def supported_features(self) -> int: @@ -383,35 +372,8 @@ class HyperionLight(LightEntity): return self._options.get(key, defaults[key]) async def async_turn_on(self, **kwargs: Any) -> None: - """Turn the lights on.""" - # == Turn device on == - # Turn on both ALL (Hyperion itself) and LEDDEVICE. It would be - # preferable to enable LEDDEVICE after the settings (e.g. brightness, - # color, effect), but this is not possible due to: - # https://github.com/hyperion-project/hyperion.ng/issues/967 - if not self.is_on: - if not await self._client.async_send_set_component( - **{ - const.KEY_COMPONENTSTATE: { - const.KEY_COMPONENT: const.KEY_COMPONENTID_ALL, - const.KEY_STATE: True, - } - } - ): - return - - if not await self._client.async_send_set_component( - **{ - const.KEY_COMPONENTSTATE: { - const.KEY_COMPONENT: const.KEY_COMPONENTID_LEDDEVICE, - const.KEY_STATE: True, - } - } - ): - return - + """Turn on the light.""" # == Get key parameters == - brightness = kwargs.get(ATTR_BRIGHTNESS, self._brightness) if ATTR_EFFECT not in kwargs and ATTR_HS_COLOR in kwargs: effect = KEY_EFFECT_SOLID else: @@ -423,20 +385,28 @@ class HyperionLight(LightEntity): rgb_color = self._rgb_color # == Set brightness == - if self._brightness != brightness: - if not await self._client.async_send_set_adjustment( - **{ - const.KEY_ADJUSTMENT: { - const.KEY_BRIGHTNESS: int( - round((float(brightness) * 100) / 255) - ) - } - } - ): - return + if ATTR_BRIGHTNESS in kwargs: + brightness = kwargs[ATTR_BRIGHTNESS] + for item in self._client.adjustment: + if const.KEY_ID in item: + if not await self._client.async_send_set_adjustment( + **{ + const.KEY_ADJUSTMENT: { + const.KEY_BRIGHTNESS: int( + round((float(brightness) * 100) / 255) + ), + const.KEY_ID: item[const.KEY_ID], + } + } + ): + return # == Set an external source - if effect and effect in const.KEY_COMPONENTID_EXTERNAL_SOURCES: + if ( + effect + and self._support_external_effects + and effect in const.KEY_COMPONENTID_EXTERNAL_SOURCES + ): # Clear any color/effect. if not await self._client.async_send_clear( @@ -484,18 +454,6 @@ class HyperionLight(LightEntity): ): return - async def async_turn_off(self, **kwargs: Any) -> None: - """Disable the LED output component.""" - if not await self._client.async_send_set_component( - **{ - const.KEY_COMPONENTSTATE: { - const.KEY_COMPONENT: const.KEY_COMPONENTID_LEDDEVICE, - const.KEY_STATE: False, - } - } - ): - return - def _set_internal_state( self, brightness: Optional[int] = None, @@ -509,17 +467,13 @@ class HyperionLight(LightEntity): self._rgb_color = rgb_color if effect is not None: self._effect = effect - if effect == KEY_EFFECT_SOLID: - self._icon = ICON_LIGHTBULB - elif effect in const.KEY_COMPONENTID_EXTERNAL_SOURCES: - self._icon = ICON_EXTERNAL_SOURCE - else: - self._icon = ICON_EFFECT + @callback def _update_components(self, _: Optional[Dict[str, Any]] = None) -> None: """Update Hyperion components.""" self.async_write_ha_state() + @callback def _update_adjustment(self, _: Optional[Dict[str, Any]] = None) -> None: """Update Hyperion adjustments.""" if self._client.adjustment: @@ -533,26 +487,31 @@ class HyperionLight(LightEntity): ) self.async_write_ha_state() + @callback def _update_priorities(self, _: Optional[Dict[str, Any]] = None) -> None: """Update Hyperion priorities.""" - visible_priority = self._client.visible_priority - if visible_priority: - componentid = visible_priority.get(const.KEY_COMPONENTID) - if componentid in const.KEY_COMPONENTID_EXTERNAL_SOURCES: + priority = self._get_priority_entry_that_dictates_state() + if priority and self._allow_priority_update(priority): + componentid = priority.get(const.KEY_COMPONENTID) + if ( + self._support_external_effects + and componentid in const.KEY_COMPONENTID_EXTERNAL_SOURCES + ): self._set_internal_state(rgb_color=DEFAULT_COLOR, effect=componentid) elif componentid == const.KEY_COMPONENTID_EFFECT: # Owner is the effect name. # See: https://docs.hyperion-project.org/en/json/ServerInfo.html#priorities self._set_internal_state( - rgb_color=DEFAULT_COLOR, effect=visible_priority[const.KEY_OWNER] + rgb_color=DEFAULT_COLOR, effect=priority[const.KEY_OWNER] ) elif componentid == const.KEY_COMPONENTID_COLOR: self._set_internal_state( - rgb_color=visible_priority[const.KEY_VALUE][const.KEY_RGB], + rgb_color=priority[const.KEY_VALUE][const.KEY_RGB], effect=KEY_EFFECT_SOLID, ) self.async_write_ha_state() + @callback def _update_effect_list(self, _: Optional[Dict[str, Any]] = None) -> None: """Update Hyperion effects.""" if not self._client.effects: @@ -562,9 +521,10 @@ class HyperionLight(LightEntity): if const.KEY_NAME in effect: effect_list.append(effect[const.KEY_NAME]) if effect_list: - self._effect_list = effect_list + self._effect_list = self._static_effect_list + effect_list self.async_write_ha_state() + @callback def _update_full_state(self) -> None: """Update full Hyperion state.""" self._update_adjustment() @@ -581,6 +541,7 @@ class HyperionLight(LightEntity): self._rgb_color, ) + @callback def _update_client(self, _: Optional[Dict[str, Any]] = None) -> None: """Update client connection state.""" self.async_write_ha_state() @@ -591,24 +552,160 @@ class HyperionLight(LightEntity): self.async_on_remove( async_dispatcher_connect( self.hass, - SIGNAL_INSTANCE_REMOVED.format(self._unique_id), + SIGNAL_ENTITY_REMOVE.format(self._unique_id), self.async_remove, ) ) - self._client.set_callbacks( - { - f"{const.KEY_ADJUSTMENT}-{const.KEY_UPDATE}": self._update_adjustment, - f"{const.KEY_COMPONENTS}-{const.KEY_UPDATE}": self._update_components, - f"{const.KEY_EFFECTS}-{const.KEY_UPDATE}": self._update_effect_list, - f"{const.KEY_PRIORITIES}-{const.KEY_UPDATE}": self._update_priorities, - f"{const.KEY_CLIENT}-{const.KEY_UPDATE}": self._update_client, - } - ) + self._client.add_callbacks(self._client_callbacks) # Load initial state. self._update_full_state() async def async_will_remove_from_hass(self) -> None: - """Disconnect from server.""" - await self._client.async_client_disconnect() + """Cleanup prior to hass removal.""" + self._client.remove_callbacks(self._client_callbacks) + + @property + def _support_external_effects(self) -> bool: + """Whether or not to support setting external effects from the light entity.""" + return True + + def _get_priority_entry_that_dictates_state(self) -> Optional[Dict[str, Any]]: + """Get the relevant Hyperion priority entry to consider.""" + # Return the visible priority (whether or not it is the HA priority). + return self._client.visible_priority # type: ignore[no-any-return] + + # pylint: disable=no-self-use + def _allow_priority_update(self, priority: Optional[Dict[str, Any]] = None) -> bool: + """Determine whether to allow a priority to update internal state.""" + return True + + +class HyperionLight(HyperionBaseLight): + """A Hyperion light that acts in absolute (vs priority) manner. + + Light state is the absolute Hyperion component state (e.g. LED device on/off) rather + than color based at a particular priority, and the 'winning' priority determines + shown state rather than exclusively the HA priority. + """ + + @property + def is_on(self) -> bool: + """Return true if light is on.""" + return ( + bool(self._client.is_on()) + and self._get_priority_entry_that_dictates_state() is not None + ) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the light.""" + # == Turn device on == + # Turn on both ALL (Hyperion itself) and LEDDEVICE. It would be + # preferable to enable LEDDEVICE after the settings (e.g. brightness, + # color, effect), but this is not possible due to: + # https://github.com/hyperion-project/hyperion.ng/issues/967 + if not bool(self._client.is_on()): + for component in [ + const.KEY_COMPONENTID_ALL, + const.KEY_COMPONENTID_LEDDEVICE, + ]: + if not await self._client.async_send_set_component( + **{ + const.KEY_COMPONENTSTATE: { + const.KEY_COMPONENT: component, + const.KEY_STATE: True, + } + } + ): + return + + # Turn on the relevant Hyperion priority as usual. + await super().async_turn_on(**kwargs) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the light.""" + if not await self._client.async_send_set_component( + **{ + const.KEY_COMPONENTSTATE: { + const.KEY_COMPONENT: const.KEY_COMPONENTID_LEDDEVICE, + const.KEY_STATE: False, + } + } + ): + return + + +class HyperionPriorityLight(HyperionBaseLight): + """A Hyperion light that only acts on a single Hyperion priority.""" + + @property + def entity_registry_enabled_default(self) -> bool: + """Whether or not the entity is enabled by default.""" + return False + + @property + def is_on(self) -> bool: + """Return true if light is on.""" + priority = self._get_priority_entry_that_dictates_state() + return ( + priority is not None + and not HyperionPriorityLight._is_priority_entry_black(priority) + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the light.""" + if not await self._client.async_send_clear( + **{const.KEY_PRIORITY: self._get_option(CONF_PRIORITY)} + ): + return + await self._client.async_send_set_color( + **{ + const.KEY_PRIORITY: self._get_option(CONF_PRIORITY), + const.KEY_COLOR: COLOR_BLACK, + const.KEY_ORIGIN: DEFAULT_ORIGIN, + } + ) + + @property + def _support_external_effects(self) -> bool: + """Whether or not to support setting external effects from the light entity.""" + return False + + def _get_priority_entry_that_dictates_state(self) -> Optional[Dict[str, Any]]: + """Get the relevant Hyperion priority entry to consider.""" + # Return the active priority (if any) at the configured HA priority. + for candidate in self._client.priorities or []: + if const.KEY_PRIORITY not in candidate: + continue + if candidate[const.KEY_PRIORITY] == self._get_option( + CONF_PRIORITY + ) and candidate.get(const.KEY_ACTIVE, False): + return candidate # type: ignore[no-any-return] + return None + + @classmethod + def _is_priority_entry_black(cls, priority: Optional[Dict[str, Any]]) -> bool: + """Determine if a given priority entry is the color black.""" + if not priority: + return False + if priority.get(const.KEY_COMPONENTID) == const.KEY_COMPONENTID_COLOR: + rgb_color = priority.get(const.KEY_VALUE, {}).get(const.KEY_RGB) + if rgb_color is not None and tuple(rgb_color) == COLOR_BLACK: + return True + return False + + # pylint: disable=no-self-use + def _allow_priority_update(self, priority: Optional[Dict[str, Any]] = None) -> bool: + """Determine whether to allow a Hyperion priority to update entity attributes.""" + # Black is treated as 'off' (and Home Assistant does not support selecting black + # from the color selector). Do not set our internal attributes if the priority is + # 'off' (i.e. if black is active). Do this to ensure it seamlessly turns back on + # at the correct prior color on the next 'on' call. + return not HyperionPriorityLight._is_priority_entry_black(priority) + + +LIGHT_TYPES = { + TYPE_HYPERION_LIGHT: HyperionLight, + TYPE_HYPERION_PRIORITY_LIGHT: HyperionPriorityLight, +} diff --git a/homeassistant/components/hyperion/manifest.json b/homeassistant/components/hyperion/manifest.json index 5f5e8ea6221..d2983e75630 100644 --- a/homeassistant/components/hyperion/manifest.json +++ b/homeassistant/components/hyperion/manifest.json @@ -1,19 +1,15 @@ { - "codeowners": [ - "@dermotduffy" - ], + "codeowners": ["@dermotduffy"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/hyperion", "domain": "hyperion", "name": "Hyperion", "quality_scale": "platinum", - "requirements": [ - "hyperion-py==0.6.1" - ], + "requirements": ["hyperion-py==0.7.0"], "ssdp": [ { "manufacturer": "Hyperion Open Source Ambient Lighting", "st": "urn:hyperion-project.org:device:basic:1" } ] -} \ No newline at end of file +} diff --git a/homeassistant/components/hyperion/switch.py b/homeassistant/components/hyperion/switch.py new file mode 100644 index 00000000000..372e9876c35 --- /dev/null +++ b/homeassistant/components/hyperion/switch.py @@ -0,0 +1,210 @@ +"""Switch platform for Hyperion.""" + +from typing import Any, Callable, Dict, Optional + +from hyperion import client +from hyperion.const import ( + KEY_COMPONENT, + KEY_COMPONENTID_ALL, + KEY_COMPONENTID_BLACKBORDER, + KEY_COMPONENTID_BOBLIGHTSERVER, + KEY_COMPONENTID_FORWARDER, + KEY_COMPONENTID_GRABBER, + KEY_COMPONENTID_LEDDEVICE, + KEY_COMPONENTID_SMOOTHING, + KEY_COMPONENTID_V4L, + KEY_COMPONENTS, + KEY_COMPONENTSTATE, + KEY_ENABLED, + KEY_NAME, + KEY_STATE, + KEY_UPDATE, +) + +from homeassistant.components.switch import SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util import slugify + +from . import get_hyperion_unique_id, listen_for_instance_updates +from .const import ( + COMPONENT_TO_NAME, + CONF_INSTANCE_CLIENTS, + DOMAIN, + NAME_SUFFIX_HYPERION_COMPONENT_SWITCH, + SIGNAL_ENTITY_REMOVE, + TYPE_HYPERION_COMPONENT_SWITCH_BASE, +) + +COMPONENT_SWITCHES = [ + KEY_COMPONENTID_ALL, + KEY_COMPONENTID_SMOOTHING, + KEY_COMPONENTID_BLACKBORDER, + KEY_COMPONENTID_FORWARDER, + KEY_COMPONENTID_BOBLIGHTSERVER, + KEY_COMPONENTID_GRABBER, + KEY_COMPONENTID_LEDDEVICE, + KEY_COMPONENTID_V4L, +] + + +async def async_setup_entry( + hass: HomeAssistantType, config_entry: ConfigEntry, async_add_entities: Callable +) -> bool: + """Set up a Hyperion platform from config entry.""" + entry_data = hass.data[DOMAIN][config_entry.entry_id] + server_id = config_entry.unique_id + + def component_to_switch_type(component: str) -> str: + """Convert a component to a switch type string.""" + return slugify( + f"{TYPE_HYPERION_COMPONENT_SWITCH_BASE} {COMPONENT_TO_NAME[component]}" + ) + + def component_to_unique_id(component: str, instance_num: int) -> str: + """Convert a component to a unique_id.""" + assert server_id + return get_hyperion_unique_id( + server_id, instance_num, component_to_switch_type(component) + ) + + def component_to_switch_name(component: str, instance_name: str) -> str: + """Convert a component to a switch name.""" + return ( + f"{instance_name} " + f"{NAME_SUFFIX_HYPERION_COMPONENT_SWITCH} " + f"{COMPONENT_TO_NAME.get(component, component.capitalize())}" + ) + + @callback + def instance_add(instance_num: int, instance_name: str) -> None: + """Add entities for a new Hyperion instance.""" + assert server_id + switches = [] + for component in COMPONENT_SWITCHES: + switches.append( + HyperionComponentSwitch( + component_to_unique_id(component, instance_num), + component_to_switch_name(component, instance_name), + component, + entry_data[CONF_INSTANCE_CLIENTS][instance_num], + ), + ) + async_add_entities(switches) + + @callback + def instance_remove(instance_num: int) -> None: + """Remove entities for an old Hyperion instance.""" + assert server_id + for component in COMPONENT_SWITCHES: + async_dispatcher_send( + hass, + SIGNAL_ENTITY_REMOVE.format( + component_to_unique_id(component, instance_num), + ), + ) + + listen_for_instance_updates(hass, config_entry, instance_add, instance_remove) + return True + + +class HyperionComponentSwitch(SwitchEntity): + """ComponentBinarySwitch switch class.""" + + def __init__( + self, + unique_id: str, + name: str, + component_name: str, + hyperion_client: client.HyperionClient, + ) -> None: + """Initialize the switch.""" + self._unique_id = unique_id + self._name = name + self._component_name = component_name + self._client = hyperion_client + self._client_callbacks = { + f"{KEY_COMPONENTS}-{KEY_UPDATE}": self._update_components + } + + @property + def should_poll(self) -> bool: + """Return whether or not this entity should be polled.""" + return False + + @property + def entity_registry_enabled_default(self) -> bool: + """Whether or not the entity is enabled by default.""" + # These component controls are for advanced users and are disabled by default. + return False + + @property + def unique_id(self) -> str: + """Return a unique id for this instance.""" + return self._unique_id + + @property + def name(self) -> str: + """Return the name of the switch.""" + return self._name + + @property + def is_on(self) -> bool: + """Return true if the switch is on.""" + for component in self._client.components: + if component[KEY_NAME] == self._component_name: + return bool(component.setdefault(KEY_ENABLED, False)) + return False + + @property + def available(self) -> bool: + """Return server availability.""" + return bool(self._client.has_loaded_state) + + async def _async_send_set_component(self, value: bool) -> None: + """Send a component control request.""" + await self._client.async_send_set_component( + **{ + KEY_COMPONENTSTATE: { + KEY_COMPONENT: self._component_name, + KEY_STATE: value, + } + } + ) + + # pylint: disable=unused-argument + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the switch.""" + await self._async_send_set_component(True) + + # pylint: disable=unused-argument + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the switch.""" + await self._async_send_set_component(False) + + @callback + def _update_components(self, _: Optional[Dict[str, Any]] = None) -> None: + """Update Hyperion components.""" + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Register callbacks when entity added to hass.""" + assert self.hass + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_ENTITY_REMOVE.format(self._unique_id), + self.async_remove, + ) + ) + + self._client.add_callbacks(self._client_callbacks) + + async def async_will_remove_from_hass(self) -> None: + """Cleanup prior to hass removal.""" + self._client.remove_callbacks(self._client_callbacks) diff --git a/homeassistant/components/hyperion/translations/fr.json b/homeassistant/components/hyperion/translations/fr.json index 8c1cb919d11..90733a8968b 100644 --- a/homeassistant/components/hyperion/translations/fr.json +++ b/homeassistant/components/hyperion/translations/fr.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "auth_new_token_not_work_error": "\u00c9chec de l'authentification \u00e0 l'aide du jeton nouvellement cr\u00e9\u00e9", + "cannot_connect": "Echec de connection" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/hyperion/translations/it.json b/homeassistant/components/hyperion/translations/it.json index ff3170ffb98..6fee49ebe14 100644 --- a/homeassistant/components/hyperion/translations/it.json +++ b/homeassistant/components/hyperion/translations/it.json @@ -8,7 +8,7 @@ "auth_required_error": "Impossibile determinare se \u00e8 necessaria l'autorizzazione", "cannot_connect": "Impossibile connettersi", "no_id": "L'istanza Hyperion Ambilight non ha segnalato il suo ID", - "reauth_successful": "Ri-autenticazione completata con successo" + "reauth_successful": "La riautenticazione ha avuto successo" }, "error": { "cannot_connect": "Impossibile connettersi", diff --git a/homeassistant/components/hyperion/translations/tr.json b/homeassistant/components/hyperion/translations/tr.json index 6f46000e3e2..7b3f9f845a1 100644 --- a/homeassistant/components/hyperion/translations/tr.json +++ b/homeassistant/components/hyperion/translations/tr.json @@ -2,11 +2,17 @@ "config": { "abort": { "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", "auth_new_token_not_granted_error": "Hyperion UI'de yeni olu\u015fturulan belirte\u00e7 onaylanmad\u0131", "auth_new_token_not_work_error": "Yeni olu\u015fturulan belirte\u00e7 kullan\u0131larak kimlik do\u011frulamas\u0131 ba\u015far\u0131s\u0131z oldu", "auth_required_error": "Yetkilendirmenin gerekli olup olmad\u0131\u011f\u0131 belirlenemedi", "cannot_connect": "Ba\u011flanma hatas\u0131", - "no_id": "Hyperion Ambilight \u00f6rne\u011fi kimli\u011fini bildirmedi" + "no_id": "Hyperion Ambilight \u00f6rne\u011fi kimli\u011fini bildirmedi", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_access_token": "Ge\u00e7ersiz eri\u015fim belirteci" }, "step": { "auth": { @@ -15,6 +21,9 @@ "token": "Veya \u00f6nceden varolan belirte\u00e7 leri sa\u011flay\u0131n" } }, + "confirm": { + "title": "Hyperion Ambilight hizmetinin eklenmesini onaylay\u0131n" + }, "create_token": { "title": "Otomatik olarak yeni kimlik do\u011frulama belirteci olu\u015fturun" }, @@ -23,6 +32,7 @@ }, "user": { "data": { + "host": "Ana Bilgisayar", "port": "Port" } } diff --git a/homeassistant/components/hyperion/translations/uk.json b/homeassistant/components/hyperion/translations/uk.json new file mode 100644 index 00000000000..ae44b0610da --- /dev/null +++ b/homeassistant/components/hyperion/translations/uk.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "auth_new_token_not_granted_error": "\u0421\u0442\u0432\u043e\u0440\u0435\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u043d\u0435 \u0431\u0443\u0432 \u0441\u0445\u0432\u0430\u043b\u0435\u043d\u0438\u0439 \u0432 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0456 Hyperion.", + "auth_new_token_not_work_error": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u043f\u0440\u043e\u0439\u0442\u0438 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0437 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f\u043c \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043e\u0433\u043e \u0442\u043e\u043a\u0435\u043d\u0430.", + "auth_required_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0432\u0438\u0437\u043d\u0430\u0447\u0438\u0442\u0438, \u0447\u0438 \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u044f.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "no_id": "Hyperion Ambilight \u043d\u0435 \u043d\u0430\u0434\u0430\u0432 \u0441\u0432\u0456\u0439 \u0456\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_access_token": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443." + }, + "step": { + "auth": { + "data": { + "create_token": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043d\u043e\u0432\u0438\u0439 \u0442\u043e\u043a\u0435\u043d", + "token": "\u0410\u0431\u043e \u043d\u0430\u0434\u0430\u0442\u0438 \u043d\u0430\u044f\u0432\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d" + }, + "description": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u044f \u043d\u0430 \u0441\u0435\u0440\u0432\u0435\u0440\u0456 Hyperion Ambilight." + }, + "confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 Hyperion Ambilight? \n\n ** \u0425\u043e\u0441\u0442: ** {host}\n ** \u041f\u043e\u0440\u0442: ** {port}\n ** ID **: {id}", + "title": "\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0456\u0442\u044c \u0434\u043e\u0434\u0430\u0432\u0430\u043d\u043d\u044f \u0441\u043b\u0443\u0436\u0431\u0438 Hyperion Ambilight" + }, + "create_token": { + "description": "\u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c ** \u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0438 ** \u043d\u0438\u0436\u0447\u0435, \u0449\u043e\u0431 \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u043d\u043e\u0432\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457. \u0412\u0438 \u0431\u0443\u0434\u0435\u0442\u0435 \u043f\u0435\u0440\u0435\u043d\u0430\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0456 \u0432 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 Hyperion \u0434\u043b\u044f \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f \u0437\u0430\u043f\u0438\u0442\u0443. \u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u043f\u043e\u043a\u0430\u0437\u0430\u043d\u0438\u0439 \u0456\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 - \" {auth_id} \"", + "title": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043d\u043e\u0432\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + }, + "create_token_external": { + "title": "\u041f\u0440\u0438\u0439\u043d\u044f\u0442\u0438 \u043d\u043e\u0432\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0432 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0456 Hyperion" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "priority": "\u041f\u0440\u0456\u043e\u0440\u0438\u0442\u0435\u0442 Hyperion \u0434\u043b\u044f \u043a\u043e\u043b\u044c\u043e\u0440\u0456\u0432 \u0456 \u0435\u0444\u0435\u043a\u0442\u0456\u0432" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/translations/de.json b/homeassistant/components/iaqualink/translations/de.json index e7e1002015c..0a678baf7ca 100644 --- a/homeassistant/components/iaqualink/translations/de.json +++ b/homeassistant/components/iaqualink/translations/de.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/iaqualink/translations/tr.json b/homeassistant/components/iaqualink/translations/tr.json new file mode 100644 index 00000000000..c2c70f3e45b --- /dev/null +++ b/homeassistant/components/iaqualink/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "L\u00fctfen iAqualink hesab\u0131n\u0131z i\u00e7in kullan\u0131c\u0131 ad\u0131 ve parolay\u0131 girin." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/translations/uk.json b/homeassistant/components/iaqualink/translations/uk.json new file mode 100644 index 00000000000..b855d755726 --- /dev/null +++ b/homeassistant/components/iaqualink/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "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." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043b\u043e\u0433\u0456\u043d \u0456 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f \u0412\u0430\u0448\u043e\u0433\u043e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 iAqualink.", + "title": "Jandy iAqualink" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/translations/de.json b/homeassistant/components/icloud/translations/de.json index e7441792d91..64a6bcd885c 100644 --- a/homeassistant/components/icloud/translations/de.json +++ b/homeassistant/components/icloud/translations/de.json @@ -1,10 +1,12 @@ { "config": { "abort": { - "already_configured": "Konto bereits konfiguriert", - "no_device": "Auf keinem Ihrer Ger\u00e4te ist \"Find my iPhone\" aktiviert" + "already_configured": "Konto wurde bereits konfiguriert", + "no_device": "Auf keinem Ihrer Ger\u00e4te ist \"Find my iPhone\" aktiviert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung", "send_verification_code": "Fehler beim Senden des Best\u00e4tigungscodes", "validate_verification_code": "Verifizierung des Verifizierungscodes fehlgeschlagen. W\u00e4hle ein vertrauensw\u00fcrdiges Ger\u00e4t aus und starte die Verifizierung erneut" }, @@ -12,7 +14,8 @@ "reauth": { "data": { "password": "Passwort" - } + }, + "title": "Integration erneut authentifizieren" }, "trusted_device": { "data": { diff --git a/homeassistant/components/icloud/translations/tr.json b/homeassistant/components/icloud/translations/tr.json index 3d74852ce50..86581625d96 100644 --- a/homeassistant/components/icloud/translations/tr.json +++ b/homeassistant/components/icloud/translations/tr.json @@ -1,7 +1,28 @@ { "config": { "abort": { - "no_device": "Hi\u00e7bir cihaz\u0131n\u0131zda \"iPhone'umu bul\" etkin de\u011fil" + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "no_device": "Hi\u00e7bir cihaz\u0131n\u0131zda \"iPhone'umu bul\" etkin de\u011fil", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "validate_verification_code": "Do\u011frulama kodunuzu do\u011frulamay\u0131 ba\u015faramad\u0131n\u0131z, bir g\u00fcven ayg\u0131t\u0131 se\u00e7in ve do\u011frulamay\u0131 yeniden ba\u015flat\u0131n" + }, + "step": { + "reauth": { + "data": { + "password": "Parola" + }, + "description": "{username} i\u00e7in \u00f6nceden girdi\u011finiz \u015fifreniz art\u0131k \u00e7al\u0131\u015fm\u0131yor. Bu entegrasyonu kullanmaya devam etmek i\u00e7in \u015fifrenizi g\u00fcncelleyin." + }, + "user": { + "data": { + "password": "Parola", + "username": "E-posta", + "with_family": "Aileyle" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/icloud/translations/uk.json b/homeassistant/components/icloud/translations/uk.json new file mode 100644 index 00000000000..ac65157f050 --- /dev/null +++ b/homeassistant/components/icloud/translations/uk.json @@ -0,0 +1,46 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "no_device": "\u041d\u0430 \u0436\u043e\u0434\u043d\u043e\u043c\u0443 \u0437 \u0412\u0430\u0448\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 \u043d\u0435 \u0430\u043a\u0442\u0438\u0432\u043e\u0432\u0430\u043d\u0430 \u0444\u0443\u043d\u043a\u0446\u0456\u044f \"\u0417\u043d\u0430\u0439\u0442\u0438 iPhone\".", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "send_verification_code": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u0438\u0442\u0438 \u043a\u043e\u0434 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f.", + "validate_verification_code": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0438\u0442\u0438 \u043a\u043e\u0434 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f, \u0432\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0434\u043e\u0432\u0456\u0440\u0435\u043d\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0442\u0430 \u043f\u043e\u0447\u043d\u0456\u0442\u044c \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0443 \u0437\u043d\u043e\u0432\u0443." + }, + "step": { + "reauth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0420\u0430\u043d\u0456\u0448\u0435 \u0432\u0432\u0435\u0434\u0435\u043d\u0438\u0439 \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043b\u044f {username} \u0431\u0456\u043b\u044c\u0448\u0435 \u043d\u0435 \u043f\u0440\u0430\u0446\u044e\u0454. \u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c, \u0449\u043e\u0431 \u043f\u0440\u043e\u0434\u043e\u0432\u0436\u0438\u0442\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0446\u044e \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0432\u0430\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e" + }, + "trusted_device": { + "data": { + "trusted_device": "\u0414\u043e\u0432\u0456\u0440\u0435\u043d\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0434\u043e\u0432\u0456\u0440\u0435\u043d\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439", + "title": "\u0414\u043e\u0432\u0456\u0440\u0435\u043d\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 iCloud" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438", + "with_family": "\u0417 \u0441\u0456\u043c'\u0454\u044e" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0412\u0430\u0448\u0456 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0456 \u0434\u0430\u043d\u0456.", + "title": "\u041e\u0431\u043b\u0456\u043a\u043e\u0432\u0456 \u0434\u0430\u043d\u0456 iCloud" + }, + "verification_code": { + "data": { + "verification_code": "\u041a\u043e\u0434 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043a\u043e\u0434 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f, \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u0438\u0439 \u0432\u0456\u0434 iCloud", + "title": "\u041a\u043e\u0434 \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f iCloud" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/translations/de.json b/homeassistant/components/ifttt/translations/de.json index c96928afa18..5184e89f29a 100644 --- a/homeassistant/components/ifttt/translations/de.json +++ b/homeassistant/components/ifttt/translations/de.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", + "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." + }, "create_entry": { "default": "Um Ereignisse an Home Assistant zu senden, musst du die Aktion \"Eine Webanforderung erstellen\" aus dem [IFTTT Webhook Applet]({applet_url}) ausw\u00e4hlen.\n\nF\u00fclle folgende Informationen aus: \n- URL: `{webhook_url}`\n- Methode: POST\n- Inhaltstyp: application/json\n\nIn der Dokumentation ({docs_url}) findest du Informationen zur Konfiguration der Automation eingehender Daten." }, diff --git a/homeassistant/components/ifttt/translations/tr.json b/homeassistant/components/ifttt/translations/tr.json new file mode 100644 index 00000000000..84adcdf8225 --- /dev/null +++ b/homeassistant/components/ifttt/translations/tr.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/translations/uk.json b/homeassistant/components/ifttt/translations/uk.json new file mode 100644 index 00000000000..8ea8f2b1970 --- /dev/null +++ b/homeassistant/components/ifttt/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "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.", + "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c." + }, + "create_entry": { + "default": "\u0414\u043b\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043a\u0438 \u043f\u043e\u0434\u0456\u0439 \u0432 Home Assistant \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0434\u0456\u044e \"Make a web request\" \u0437 [IFTTT Webhook applet]({applet_url}). \n\n\u0417\u0430\u043f\u043e\u0432\u043d\u0456\u0442\u044c \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e: \n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json \n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0456\u0439 \u043f\u043e \u043e\u0431\u0440\u043e\u0431\u0446\u0456 \u0434\u0430\u043d\u0438\u0445, \u0449\u043e \u043d\u0430\u0434\u0445\u043e\u0434\u044f\u0442\u044c." + }, + "step": { + "user": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 IFTTT?", + "title": "IFTTT" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/image/manifest.json b/homeassistant/components/image/manifest.json index 246ea387140..6978f09ab68 100644 --- a/homeassistant/components/image/manifest.json +++ b/homeassistant/components/image/manifest.json @@ -3,7 +3,7 @@ "name": "Image", "config_flow": false, "documentation": "https://www.home-assistant.io/integrations/image", - "requirements": ["pillow==7.2.0"], + "requirements": ["pillow==8.1.0"], "dependencies": ["http"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal" diff --git a/homeassistant/components/input_boolean/translations/uk.json b/homeassistant/components/input_boolean/translations/uk.json index c677957de47..be22ae53807 100644 --- a/homeassistant/components/input_boolean/translations/uk.json +++ b/homeassistant/components/input_boolean/translations/uk.json @@ -5,5 +5,5 @@ "on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e" } }, - "title": "\u0412\u0432\u0435\u0434\u0435\u043d\u043d\u044f \u043b\u043e\u0433\u0456\u0447\u043d\u043e\u0433\u043e \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f" + "title": "Input Boolean" } \ No newline at end of file diff --git a/homeassistant/components/input_datetime/__init__.py b/homeassistant/components/input_datetime/__init__.py index 195e4c2242e..0eab810245d 100644 --- a/homeassistant/components/input_datetime/__init__.py +++ b/homeassistant/components/input_datetime/__init__.py @@ -32,8 +32,6 @@ CONF_HAS_DATE = "has_date" CONF_HAS_TIME = "has_time" CONF_INITIAL = "initial" -DEFAULT_VALUE = "1970-01-01 00:00:00" -DEFAULT_DATE = py_datetime.date(1970, 1, 1) DEFAULT_TIME = py_datetime.time(0, 0, 0) ATTR_DATETIME = "datetime" @@ -218,7 +216,9 @@ class InputDatetime(RestoreEntity): else: time = dt_util.parse_time(initial) - current_datetime = py_datetime.datetime.combine(DEFAULT_DATE, time) + current_datetime = py_datetime.datetime.combine( + py_datetime.date.today(), time + ) # If the user passed in an initial value with a timezone, convert it to right tz if current_datetime.tzinfo is not None: @@ -246,32 +246,36 @@ class InputDatetime(RestoreEntity): if self.state is not None: return + default_value = py_datetime.datetime.today().strftime("%Y-%m-%d 00:00:00") + # Priority 2: Old state old_state = await self.async_get_last_state() if old_state is None: - self._current_datetime = dt_util.parse_datetime(DEFAULT_VALUE) + self._current_datetime = dt_util.parse_datetime(default_value) return if self.has_date and self.has_time: date_time = dt_util.parse_datetime(old_state.state) if date_time is None: - current_datetime = dt_util.parse_datetime(DEFAULT_VALUE) + current_datetime = dt_util.parse_datetime(default_value) else: current_datetime = date_time elif self.has_date: date = dt_util.parse_date(old_state.state) if date is None: - current_datetime = dt_util.parse_datetime(DEFAULT_VALUE) + current_datetime = dt_util.parse_datetime(default_value) else: current_datetime = py_datetime.datetime.combine(date, DEFAULT_TIME) else: time = dt_util.parse_time(old_state.state) if time is None: - current_datetime = dt_util.parse_datetime(DEFAULT_VALUE) + current_datetime = dt_util.parse_datetime(default_value) else: - current_datetime = py_datetime.datetime.combine(DEFAULT_DATE, time) + current_datetime = py_datetime.datetime.combine( + py_datetime.date.today(), time + ) self._current_datetime = current_datetime.replace( tzinfo=dt_util.DEFAULT_TIME_ZONE diff --git a/homeassistant/components/input_datetime/translations/uk.json b/homeassistant/components/input_datetime/translations/uk.json index bd087e535a5..c0aeb11882f 100644 --- a/homeassistant/components/input_datetime/translations/uk.json +++ b/homeassistant/components/input_datetime/translations/uk.json @@ -1,3 +1,3 @@ { - "title": "\u0412\u0432\u0435\u0434\u0435\u043d\u043d\u044f \u0434\u0430\u0442\u0438" + "title": "Input Datetime" } \ No newline at end of file diff --git a/homeassistant/components/input_number/translations/uk.json b/homeassistant/components/input_number/translations/uk.json index 0e4265d7ca0..e09531134cd 100644 --- a/homeassistant/components/input_number/translations/uk.json +++ b/homeassistant/components/input_number/translations/uk.json @@ -1,3 +1,3 @@ { - "title": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043d\u043e\u043c\u0435\u0440" + "title": "Input Number" } \ No newline at end of file diff --git a/homeassistant/components/input_select/__init__.py b/homeassistant/components/input_select/__init__.py index 9269dc3a7f9..6272992f243 100644 --- a/homeassistant/components/input_select/__init__.py +++ b/homeassistant/components/input_select/__init__.py @@ -29,10 +29,13 @@ CONF_OPTIONS = "options" ATTR_OPTION = "option" ATTR_OPTIONS = "options" +ATTR_CYCLE = "cycle" SERVICE_SELECT_OPTION = "select_option" SERVICE_SELECT_NEXT = "select_next" SERVICE_SELECT_PREVIOUS = "select_previous" +SERVICE_SELECT_FIRST = "select_first" +SERVICE_SELECT_LAST = "select_last" SERVICE_SET_OPTIONS = "set_options" STORAGE_KEY = DOMAIN STORAGE_VERSION = 1 @@ -141,14 +144,26 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: component.async_register_entity_service( SERVICE_SELECT_NEXT, - {}, - callback(lambda entity, call: entity.async_offset_index(1)), + {vol.Optional(ATTR_CYCLE, default=True): bool}, + "async_next", ) component.async_register_entity_service( SERVICE_SELECT_PREVIOUS, + {vol.Optional(ATTR_CYCLE, default=True): bool}, + "async_previous", + ) + + component.async_register_entity_service( + SERVICE_SELECT_FIRST, {}, - callback(lambda entity, call: entity.async_offset_index(-1)), + callback(lambda entity, call: entity.async_select_index(0)), + ) + + component.async_register_entity_service( + SERVICE_SELECT_LAST, + {}, + callback(lambda entity, call: entity.async_select_index(-1)), ) component.async_register_entity_service( @@ -263,13 +278,37 @@ class InputSelect(RestoreEntity): self.async_write_ha_state() @callback - def async_offset_index(self, offset): - """Offset current index.""" - current_index = self._options.index(self._current_option) - new_index = (current_index + offset) % len(self._options) + def async_select_index(self, idx): + """Select new option by index.""" + new_index = idx % len(self._options) self._current_option = self._options[new_index] self.async_write_ha_state() + @callback + def async_offset_index(self, offset, cycle): + """Offset current index.""" + current_index = self._options.index(self._current_option) + new_index = current_index + offset + if cycle: + new_index = new_index % len(self._options) + else: + if new_index < 0: + new_index = 0 + elif new_index >= len(self._options): + new_index = len(self._options) - 1 + self._current_option = self._options[new_index] + self.async_write_ha_state() + + @callback + def async_next(self, cycle): + """Select next option.""" + self.async_offset_index(1, cycle) + + @callback + def async_previous(self, cycle): + """Select previous option.""" + self.async_offset_index(-1, cycle) + @callback def async_set_options(self, options): """Set options.""" diff --git a/homeassistant/components/input_select/services.yaml b/homeassistant/components/input_select/services.yaml index f4aa44f3de0..0eddb158d34 100644 --- a/homeassistant/components/input_select/services.yaml +++ b/homeassistant/components/input_select/services.yaml @@ -4,6 +4,9 @@ select_next: entity_id: description: Entity id of the input select to select the next value for. example: input_select.my_select + cycle: + description: If the option should cycle from the last to the first (defaults to true). + example: true select_option: description: Select an option of an input select entity. fields: @@ -19,6 +22,21 @@ select_previous: entity_id: description: Entity id of the input select to select the previous value for. example: input_select.my_select + cycle: + description: If the option should cycle from the first to the last (defaults to true). + example: true +select_first: + description: Select the first option of an input select entity. + fields: + entity_id: + description: Entity id of the input select to select the first value for. + example: input_select.my_select +select_last: + description: Select the last option of an input select entity. + fields: + entity_id: + description: Entity id of the input select to select the last value for. + example: input_select.my_select set_options: description: Set the options of an input select entity. fields: diff --git a/homeassistant/components/input_select/translations/uk.json b/homeassistant/components/input_select/translations/uk.json index ace44f8d7a7..b33e64fbf48 100644 --- a/homeassistant/components/input_select/translations/uk.json +++ b/homeassistant/components/input_select/translations/uk.json @@ -1,3 +1,3 @@ { - "title": "\u0412\u0438\u0431\u0440\u0430\u0442\u0438" + "title": "Input Select" } \ No newline at end of file diff --git a/homeassistant/components/input_text/translations/uk.json b/homeassistant/components/input_text/translations/uk.json index a80f4325203..84bddfe3e07 100644 --- a/homeassistant/components/input_text/translations/uk.json +++ b/homeassistant/components/input_text/translations/uk.json @@ -1,3 +1,3 @@ { - "title": "\u0412\u0432\u0435\u0434\u0435\u043d\u043d\u044f \u0442\u0435\u043a\u0441\u0442\u0443" + "title": "Input Text" } \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/de.json b/homeassistant/components/insteon/translations/de.json index dfa4f3f7567..6bbc4d5474f 100644 --- a/homeassistant/components/insteon/translations/de.json +++ b/homeassistant/components/insteon/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "cannot_connect": "Verbindung fehlgeschlagen" + "cannot_connect": "Verbindung fehlgeschlagen", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "error": { "cannot_connect": "Verbindung fehlgeschlagen" @@ -22,6 +23,9 @@ } }, "plm": { + "data": { + "device": "USB-Ger\u00e4te-Pfad" + }, "title": "Insteon PLM" }, "user": { diff --git a/homeassistant/components/insteon/translations/tr.json b/homeassistant/components/insteon/translations/tr.json new file mode 100644 index 00000000000..6c41f53b31e --- /dev/null +++ b/homeassistant/components/insteon/translations/tr.json @@ -0,0 +1,89 @@ +{ + "config": { + "abort": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "hubv1": { + "data": { + "host": "\u0130p Adresi", + "port": "Port" + }, + "title": "Insteon Hub S\u00fcr\u00fcm 1" + }, + "hubv2": { + "data": { + "host": "\u0130p Adresi", + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "Insteon Hub S\u00fcr\u00fcm 2'yi yap\u0131land\u0131r\u0131n.", + "title": "Insteon Hub S\u00fcr\u00fcm 2" + }, + "plm": { + "description": "Insteon PowerLink Modemini (PLM) yap\u0131land\u0131r\u0131n." + }, + "user": { + "data": { + "modem_type": "Modem t\u00fcr\u00fc." + }, + "description": "Insteon modem tipini se\u00e7in.", + "title": "Insteon" + } + } + }, + "options": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "input_error": "Ge\u00e7ersiz giri\u015fler, l\u00fctfen de\u011ferlerinizi kontrol edin." + }, + "step": { + "add_x10": { + "data": { + "unitcode": "Birim kodu (1-16)" + }, + "description": "Insteon Hub parolas\u0131n\u0131 de\u011fi\u015ftirin.", + "title": "Insteon" + }, + "change_hub_config": { + "data": { + "host": "\u0130p Adresi", + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "title": "Insteon" + }, + "init": { + "data": { + "add_override": "Bir cihaz\u0131 ge\u00e7ersiz k\u0131lma ekleyin.", + "add_x10": "Bir X10 cihaz\u0131 ekleyin.", + "change_hub_config": "Hub yap\u0131land\u0131rmas\u0131n\u0131 de\u011fi\u015ftirin.", + "remove_override": "Bir cihaz\u0131 ge\u00e7ersiz k\u0131lma i\u015flemini kald\u0131r\u0131n.", + "remove_x10": "Bir X10 cihaz\u0131n\u0131 \u00e7\u0131kar\u0131n." + }, + "description": "Yap\u0131land\u0131rmak i\u00e7in bir se\u00e7enek se\u00e7in.", + "title": "Insteon" + }, + "remove_override": { + "data": { + "address": "Kald\u0131r\u0131lacak bir cihaz adresi se\u00e7in" + }, + "description": "Bir cihaz\u0131 ge\u00e7ersiz k\u0131lmay\u0131 kald\u0131rma", + "title": "Insteon" + }, + "remove_x10": { + "data": { + "address": "Kald\u0131r\u0131lacak bir cihaz adresi se\u00e7in" + }, + "description": "Bir X10 cihaz\u0131n\u0131 kald\u0131r\u0131n", + "title": "Insteon" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/translations/uk.json b/homeassistant/components/insteon/translations/uk.json new file mode 100644 index 00000000000..302d8c3676a --- /dev/null +++ b/homeassistant/components/insteon/translations/uk.json @@ -0,0 +1,109 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "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." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "select_single": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u043f\u0446\u0456\u044e." + }, + "step": { + "hubv1": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Insteon Hub \u0432\u0435\u0440\u0441\u0456\u0457 1 (\u0434\u043e 2014 \u0440\u043e\u043a\u0443)", + "title": "Insteon Hub. \u0412\u0435\u0440\u0441\u0456\u044f 1" + }, + "hubv2": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Insteon Hub \u0432\u0435\u0440\u0441\u0456\u0457 2", + "title": "Insteon Hub. \u0412\u0435\u0440\u0441\u0456\u044f 2" + }, + "plm": { + "data": { + "device": "\u0428\u043b\u044f\u0445 \u0434\u043e USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043c\u043e\u0434\u0435\u043c\u0443 Insteon PowerLink (PLM)", + "title": "Insteon PLM" + }, + "user": { + "data": { + "modem_type": "\u0422\u0438\u043f \u043c\u043e\u0434\u0435\u043c\u0443" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0442\u0438\u043f \u043c\u043e\u0434\u0435\u043c\u0443 Insteon.", + "title": "Insteon" + } + } + }, + "options": { + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "input_error": "\u041d\u0435\u0432\u0456\u0440\u043d\u0456 \u0434\u0430\u043d\u0456, \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0432\u043a\u0430\u0437\u0430\u043d\u0456 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f.", + "select_single": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u043f\u0446\u0456\u044e." + }, + "step": { + "add_override": { + "data": { + "address": "\u0410\u0434\u0440\u0435\u0441\u0430 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: 1a2b3c)", + "cat": "\u041a\u0430\u0442\u0435\u0433\u043e\u0440\u0456\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: 0x10)", + "subcat": "\u041f\u0456\u0434\u043a\u0430\u0442\u0435\u0433\u043e\u0440\u0456\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434, 0x0a)" + }, + "description": "\u041f\u0435\u0440\u0435\u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e", + "title": "Insteon" + }, + "add_x10": { + "data": { + "housecode": "\u041a\u043e\u0434 \u0431\u0443\u0434\u0438\u043d\u043a\u0443 (a - p)", + "platform": "\u041f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u0430", + "steps": "\u041a\u0440\u043e\u043a \u0434\u0456\u043c\u043c\u0435\u0440\u0430 (\u0442\u0456\u043b\u044c\u043a\u0438 \u0434\u043b\u044f \u043e\u0441\u0432\u0456\u0442\u043b\u044e\u0432\u0430\u043b\u044c\u043d\u0438\u0445 \u043f\u0440\u0438\u043b\u0430\u0434\u0456\u0432, \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c 22)", + "unitcode": "\u042e\u043d\u0456\u0442\u043a\u043e\u0434 (1 - 16)" + }, + "description": "\u0417\u043c\u0456\u043d\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c \u0434\u043e Insteon Hub", + "title": "Insteon" + }, + "change_hub_config": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0417\u043c\u0456\u043d\u0456\u0442\u044c \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f Insteon Hub. \u041f\u0456\u0441\u043b\u044f \u0432\u043d\u0435\u0441\u0435\u043d\u043d\u044f \u0446\u0438\u0445 \u0437\u043c\u0456\u043d \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0438 Home Assistant. \u0426\u0435 \u043d\u0435 \u0437\u043c\u0456\u043d\u044e\u0454 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e \u0441\u0430\u043c\u043e\u0433\u043e \u0445\u0430\u0431\u0430. \u0429\u043e\u0431 \u0437\u043c\u0456\u043d\u0438\u0442\u0438 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e \u0445\u0430\u0431\u0430, \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a Hub.", + "title": "Insteon" + }, + "init": { + "data": { + "add_override": "\u041f\u0435\u0440\u0435\u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e", + "add_x10": "\u0414\u043e\u0434\u0430\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 X10", + "change_hub_config": "\u0417\u043c\u0456\u043d\u0438\u0442\u0438 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e \u0445\u0430\u0431\u0430", + "remove_override": "\u0412\u0438\u0434\u0430\u043b\u0438\u0442\u0438 \u043f\u0435\u0440\u0435\u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e", + "remove_x10": "\u0412\u0438\u0434\u0430\u043b\u0438\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 X10" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u043f\u0446\u0456\u044e \u0434\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f", + "title": "Insteon" + }, + "remove_override": { + "data": { + "address": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e, \u044f\u043a\u0438\u0439 \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0432\u0438\u0434\u0430\u043b\u0438\u0442\u0438" + }, + "description": "\u0412\u0438\u0434\u0430\u043b\u0438\u0442\u0438 \u043f\u0435\u0440\u0435\u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e", + "title": "Insteon" + }, + "remove_x10": { + "data": { + "address": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e, \u044f\u043a\u0438\u0439 \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0432\u0438\u0434\u0430\u043b\u0438\u0442\u0438" + }, + "description": "\u0412\u0438\u0434\u0430\u043b\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e X10", + "title": "Insteon" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/insteon/utils.py b/homeassistant/components/insteon/utils.py index 7c7bf08792b..ace24644523 100644 --- a/homeassistant/components/insteon/utils.py +++ b/homeassistant/components/insteon/utils.py @@ -294,9 +294,7 @@ def async_register_services(hass): signal = f"{address.id}_{SIGNAL_REMOVE_ENTITY}" async_dispatcher_send(hass, signal) dev_registry = await hass.helpers.device_registry.async_get_registry() - device = dev_registry.async_get_device( - identifiers={(DOMAIN, str(address))}, connections=set() - ) + device = dev_registry.async_get_device(identifiers={(DOMAIN, str(address))}) if device: dev_registry.async_remove_device(device.id) diff --git a/homeassistant/components/intent/__init__.py b/homeassistant/components/intent/__init__.py index 8410303ac81..4fd6daa5102 100644 --- a/homeassistant/components/intent/__init__.py +++ b/homeassistant/components/intent/__init__.py @@ -9,8 +9,6 @@ from homeassistant.helpers import config_validation as cv, integration_platform, from .const import DOMAIN -CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) - async def async_setup(hass: HomeAssistant, config: dict): """Set up the Intent component.""" diff --git a/homeassistant/components/ios/translations/de.json b/homeassistant/components/ios/translations/de.json index e9e592d18c2..bc427bd2992 100644 --- a/homeassistant/components/ios/translations/de.json +++ b/homeassistant/components/ios/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "single_instance_allowed": "Es wird nur eine Konfiguration von Home Assistant iOS ben\u00f6tigt" + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "step": { "confirm": { diff --git a/homeassistant/components/ios/translations/tr.json b/homeassistant/components/ios/translations/tr.json new file mode 100644 index 00000000000..8de4663957e --- /dev/null +++ b/homeassistant/components/ios/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "confirm": { + "description": "Kuruluma ba\u015flamak ister misiniz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ios/translations/uk.json b/homeassistant/components/ios/translations/uk.json new file mode 100644 index 00000000000..5f8d69f5f29 --- /dev/null +++ b/homeassistant/components/ios/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "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." + }, + "step": { + "confirm": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipma/translations/lb.json b/homeassistant/components/ipma/translations/lb.json index 7b2d374b6f5..006d80d3786 100644 --- a/homeassistant/components/ipma/translations/lb.json +++ b/homeassistant/components/ipma/translations/lb.json @@ -15,5 +15,10 @@ "title": "Standuert" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "IPMA API Endpunkt ereechbar" + } } } \ No newline at end of file diff --git a/homeassistant/components/ipma/translations/tr.json b/homeassistant/components/ipma/translations/tr.json index 488ad379942..a8df63645ab 100644 --- a/homeassistant/components/ipma/translations/tr.json +++ b/homeassistant/components/ipma/translations/tr.json @@ -1,4 +1,15 @@ { + "config": { + "step": { + "user": { + "data": { + "latitude": "Enlem", + "longitude": "Boylam", + "mode": "Mod" + } + } + } + }, "system_health": { "info": { "api_endpoint_reachable": "Ula\u015f\u0131labilir IPMA API u\u00e7 noktas\u0131" diff --git a/homeassistant/components/ipma/translations/uk.json b/homeassistant/components/ipma/translations/uk.json index bb294cc5d21..ee84e7d16f2 100644 --- a/homeassistant/components/ipma/translations/uk.json +++ b/homeassistant/components/ipma/translations/uk.json @@ -1,9 +1,24 @@ { "config": { + "error": { + "name_exists": "\u0426\u044f \u043d\u0430\u0437\u0432\u0430 \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f." + }, "step": { "user": { - "title": "\u0420\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f" + "data": { + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", + "mode": "\u0420\u0435\u0436\u0438\u043c", + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "description": "\u041f\u043e\u0440\u0442\u0443\u0433\u0430\u043b\u044c\u0441\u044c\u043a\u0438\u0439 \u0456\u043d\u0441\u0442\u0438\u0442\u0443\u0442 \u043c\u043e\u0440\u044f \u0442\u0430 \u0430\u0442\u043c\u043e\u0441\u0444\u0435\u0440\u0438.", + "title": "\u041c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "\u0414\u043e\u0441\u0442\u0443\u043f \u0434\u043e API IPMA" + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/de.json b/homeassistant/components/ipp/translations/de.json index 73dd3f69bcc..69402c8fdba 100644 --- a/homeassistant/components/ipp/translations/de.json +++ b/homeassistant/components/ipp/translations/de.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", "connection_upgrade": "Verbindung zum Drucker fehlgeschlagen, da ein Verbindungsupgrade erforderlich ist.", "ipp_error": "IPP-Fehler festgestellt.", "ipp_version_error": "IPP-Version wird vom Drucker nicht unterst\u00fctzt.", @@ -9,6 +10,7 @@ "unique_id_required": "Ger\u00e4t fehlt die f\u00fcr die Entdeckung erforderliche eindeutige Identifizierung." }, "error": { + "cannot_connect": "Verbindung fehlgeschlagen", "connection_upgrade": "Verbindung zum Drucker fehlgeschlagen. Bitte versuchen Sie es erneut mit aktivierter SSL / TLS-Option." }, "flow_title": "Drucker: {name}", @@ -18,14 +20,14 @@ "base_path": "Relativer Pfad zum Drucker", "host": "Host", "port": "Port", - "ssl": "Der Drucker unterst\u00fctzt die Kommunikation \u00fcber SSL / TLS", - "verify_ssl": "Der Drucker verwendet ein ordnungsgem\u00e4\u00dfes SSL-Zertifikat" + "ssl": "Verwendet ein SSL-Zertifikat", + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" }, "description": "Richten Sie Ihren Drucker \u00fcber das Internet Printing Protocol (IPP) f\u00fcr die Integration in Home Assistant ein.", "title": "Verbinden Sie Ihren Drucker" }, "zeroconf_confirm": { - "description": "M\u00f6chten Sie den Drucker mit dem Namen \"{name}\" zu Home Assistant hinzuf\u00fcgen?", + "description": "M\u00f6chtest du {name} einrichten?", "title": "Entdeckter Drucker" } } diff --git a/homeassistant/components/ipp/translations/tr.json b/homeassistant/components/ipp/translations/tr.json index dbb14fe825e..78b9a868bd2 100644 --- a/homeassistant/components/ipp/translations/tr.json +++ b/homeassistant/components/ipp/translations/tr.json @@ -1,7 +1,24 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "connection_upgrade": "Yaz\u0131c\u0131ya ba\u011flan\u0131lamad\u0131. L\u00fctfen SSL / TLS se\u00e7ene\u011fi i\u015faretli olarak tekrar deneyin." + }, + "flow_title": "Yaz\u0131c\u0131: {name}", "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + }, + "title": "Yaz\u0131c\u0131n\u0131z\u0131 ba\u011flay\u0131n" + }, "zeroconf_confirm": { + "description": "{name} kurmak istiyor musunuz?", "title": "Ke\u015ffedilen yaz\u0131c\u0131" } } diff --git a/homeassistant/components/ipp/translations/uk.json b/homeassistant/components/ipp/translations/uk.json new file mode 100644 index 00000000000..bb6df07f1e4 --- /dev/null +++ b/homeassistant/components/ipp/translations/uk.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "connection_upgrade": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0456\u0437 \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u043e\u043c \u0447\u0435\u0440\u0435\u0437 \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u0456\u0441\u0442\u044c \u043f\u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f.", + "ipp_error": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 IPP.", + "ipp_version_error": "\u0412\u0435\u0440\u0441\u0456\u044f IPP \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u043e\u043c.", + "parse_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0440\u043e\u0437\u0456\u0431\u0440\u0430\u0442\u0438 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u044c \u0432\u0456\u0434 \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430.", + "unique_id_required": "\u041d\u0430 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u0432\u0456\u0434\u0441\u0443\u0442\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044f, \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u0430 \u0434\u043b\u044f \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "connection_upgrade": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0456\u0437 \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u043e\u043c. \u0421\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f \u0447\u0435\u0440\u0435\u0437 SSL / TLS." + }, + "flow_title": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440: {name}", + "step": { + "user": { + "data": { + "base_path": "\u0412\u0456\u0434\u043d\u043e\u0441\u043d\u0438\u0439 \u0448\u043b\u044f\u0445 \u0434\u043e \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430", + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL", + "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430 \u043f\u043e \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 IPP.", + "title": "Internet Printing Protocol (IPP)" + }, + "zeroconf_confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 \u043f\u0440\u0438\u043d\u0442\u0435\u0440 `{name}`?", + "title": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u043f\u0440\u0438\u043d\u0442\u0435\u0440" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/translations/tr.json b/homeassistant/components/iqvia/translations/tr.json new file mode 100644 index 00000000000..717f6d72b94 --- /dev/null +++ b/homeassistant/components/iqvia/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/iqvia/translations/uk.json b/homeassistant/components/iqvia/translations/uk.json new file mode 100644 index 00000000000..ab9813d6289 --- /dev/null +++ b/homeassistant/components/iqvia/translations/uk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "invalid_zip_code": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043f\u043e\u0448\u0442\u043e\u0432\u0438\u0439 \u0456\u043d\u0434\u0435\u043a\u0441." + }, + "step": { + "user": { + "data": { + "zip_code": "\u041f\u043e\u0448\u0442\u043e\u0432\u0438\u0439 \u0456\u043d\u0434\u0435\u043a\u0441" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0441\u0432\u0456\u0439 \u043f\u043e\u0448\u0442\u043e\u0432\u0438\u0439 \u0456\u043d\u0434\u0435\u043a\u0441 (\u0434\u043b\u044f \u0421\u0428\u0410 \u0430\u0431\u043e \u041a\u0430\u043d\u0430\u0434\u0438).", + "title": "IQVIA" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/islamic_prayer_times/translations/de.json b/homeassistant/components/islamic_prayer_times/translations/de.json index af38303c9a2..b06137bdb0e 100644 --- a/homeassistant/components/islamic_prayer_times/translations/de.json +++ b/homeassistant/components/islamic_prayer_times/translations/de.json @@ -1,3 +1,8 @@ { + "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + } + }, "title": "Islamische Gebetszeiten" } \ No newline at end of file diff --git a/homeassistant/components/islamic_prayer_times/translations/tr.json b/homeassistant/components/islamic_prayer_times/translations/tr.json new file mode 100644 index 00000000000..a152eb19468 --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/islamic_prayer_times/translations/uk.json b/homeassistant/components/islamic_prayer_times/translations/uk.json new file mode 100644 index 00000000000..9290114899a --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/translations/uk.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "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." + }, + "step": { + "user": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 \u0440\u043e\u0437\u043a\u043b\u0430\u0434 \u0447\u0430\u0441\u0443 \u043d\u0430\u043c\u0430\u0437\u0443?", + "title": "\u0427\u0430\u0441 \u043d\u0430\u043c\u0430\u0437\u0443" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "calculation_method": "\u0421\u043f\u043e\u0441\u0456\u0431 \u0440\u043e\u0437\u0440\u0430\u0445\u0443\u043d\u043a\u0443" + } + } + } + }, + "title": "\u0427\u0430\u0441 \u043d\u0430\u043c\u0430\u0437\u0443" +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/de.json b/homeassistant/components/isy994/translations/de.json index 99d11e5d6c9..18d6a1603c4 100644 --- a/homeassistant/components/isy994/translations/de.json +++ b/homeassistant/components/isy994/translations/de.json @@ -18,7 +18,7 @@ "username": "Benutzername" }, "description": "Der Hosteintrag muss im vollst\u00e4ndigen URL-Format vorliegen, z. B. http://192.168.10.100:80", - "title": "Stellen Sie eine Verbindung zu Ihrem ISY994 her" + "title": "Stelle eine Verbindung zu deinem ISY994 her" } } }, diff --git a/homeassistant/components/isy994/translations/tr.json b/homeassistant/components/isy994/translations/tr.json new file mode 100644 index 00000000000..d1423202fe0 --- /dev/null +++ b/homeassistant/components/isy994/translations/tr.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "URL", + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "variable_sensor_string": "De\u011fi\u015fken Sens\u00f6r Dizesi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/isy994/translations/uk.json b/homeassistant/components/isy994/translations/uk.json new file mode 100644 index 00000000000..c874b8654f5 --- /dev/null +++ b/homeassistant/components/isy994/translations/uk.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "invalid_host": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043c\u0430\u0454 \u0431\u0443\u0442\u0438 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 'address[:port]' (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: 'http://192.168.10.100:80').", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "flow_title": "Universal Devices ISY994 {name} ({host})", + "step": { + "user": { + "data": { + "host": "URL-\u0430\u0434\u0440\u0435\u0441\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "tls": "\u0412\u0435\u0440\u0441\u0456\u044f TLS \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0412\u043a\u0430\u0436\u0456\u0442\u044c URL-\u0430\u0434\u0440\u0435\u0441\u0443 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 'address[:port]' (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: 'http://192.168.10.100:80').", + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "ignore_string": "\u0406\u0433\u043d\u043e\u0440\u0443\u0432\u0430\u0442\u0438", + "restore_light_state": "\u0412\u0456\u0434\u043d\u043e\u0432\u043b\u044e\u0432\u0430\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c \u0441\u0432\u0456\u0442\u043b\u0430", + "sensor_string": "\u0406\u043c\u043f\u043e\u0440\u0442\u0443\u0432\u0430\u0442\u0438 \u0432\u0443\u0437\u043e\u043b \u044f\u043a \u0441\u0435\u043d\u0441\u043e\u0440", + "variable_sensor_string": "\u0406\u043c\u043f\u043e\u0440\u0442\u0443\u0432\u0430\u0442\u0438 \u0437\u043c\u0456\u043d\u043d\u0443 \u044f\u043a \u0441\u0435\u043d\u0441\u043e\u0440" + }, + "description": "\u041e\u043f\u0438\u0441 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432:\n \u2022 \u0406\u043c\u043f\u043e\u0440\u0442\u0443\u0432\u0430\u0442\u0438 \u0432\u0443\u0437\u043e\u043b \u044f\u043a \u0441\u0435\u043d\u0441\u043e\u0440: \u0431\u0443\u0434\u044c-\u044f\u043a\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0430\u0431\u043e \u043f\u0430\u043f\u043a\u0430, \u0432 \u0456\u043c\u0435\u043d\u0456 \u044f\u043a\u043e\u0457 \u043c\u0456\u0441\u0442\u0438\u0442\u044c\u0441\u044f \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u0440\u044f\u0434\u043e\u043a, \u0431\u0443\u0434\u0435 \u0456\u043c\u043f\u043e\u0440\u0442\u043e\u0432\u0430\u043d\u043e \u044f\u043a \u0441\u0435\u043d\u0441\u043e\u0440 \u0430\u0431\u043e \u0431\u0456\u043d\u0430\u0440\u043d\u0438\u0439 \u0441\u0435\u043d\u0441\u043e\u0440.\n \u2022 \u0406\u043c\u043f\u043e\u0440\u0442\u0443\u0432\u0430\u0442\u0438 \u0437\u043c\u0456\u043d\u043d\u0443 \u044f\u043a \u0441\u0435\u043d\u0441\u043e\u0440: \u0431\u0443\u0434\u044c-\u044f\u043a\u0430 \u0437\u043c\u0456\u043d\u043d\u0430, \u044f\u043a\u0430 \u043c\u0456\u0441\u0442\u0438\u0442\u044c \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u0440\u044f\u0434\u043e\u043a, \u0431\u0443\u0434\u0435 \u0456\u043c\u043f\u043e\u0440\u0442\u043e\u0432\u0430\u043d\u0430 \u044f\u043a \u0441\u0435\u043d\u0441\u043e\u0440.\n \u2022 \u0406\u0433\u043d\u043e\u0440\u0443\u0432\u0430\u0442\u0438: \u0431\u0443\u0434\u044c-\u044f\u043a\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439, \u0432 \u0456\u043c\u0435\u043d\u0456 \u044f\u043a\u043e\u0433\u043e \u043c\u0456\u0441\u0442\u0438\u0442\u044c\u0441\u044f \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0439 \u0440\u044f\u0434\u043e\u043a, \u0431\u0443\u0434\u0435 \u0456\u0433\u043d\u043e\u0440\u0443\u0432\u0430\u0442\u0438\u0441\u044f.\n \u2022 \u0412\u0456\u0434\u043d\u043e\u0432\u043b\u044e\u0432\u0430\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c \u0441\u0432\u0456\u0442\u043b\u0430: \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u0456 \u043e\u0441\u0432\u0456\u0442\u043b\u0435\u043d\u043d\u044f \u0431\u0443\u0434\u0435 \u0432\u0456\u0434\u043d\u043e\u0432\u043b\u0435\u043d\u043e \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u044f\u0441\u043a\u0440\u0430\u0432\u043e\u0441\u0442\u0456, \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0435 \u0434\u043e \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f ISY994" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/config_flow.py b/homeassistant/components/izone/config_flow.py index add1bb47a54..a64356051d0 100644 --- a/homeassistant/components/izone/config_flow.py +++ b/homeassistant/components/izone/config_flow.py @@ -6,6 +6,7 @@ import logging from async_timeout import timeout from homeassistant import config_entries +from homeassistant.core import callback from homeassistant.helpers import config_entry_flow from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -18,9 +19,12 @@ _LOGGER = logging.getLogger(__name__) async def _async_has_devices(hass): controller_ready = asyncio.Event() - async_dispatcher_connect( - hass, DISPATCH_CONTROLLER_DISCOVERED, lambda x: controller_ready.set() - ) + + @callback + def dispatch_discovered(_): + controller_ready.set() + + async_dispatcher_connect(hass, DISPATCH_CONTROLLER_DISCOVERED, dispatch_discovered) disco = await async_start_discovery_service(hass) diff --git a/homeassistant/components/izone/manifest.json b/homeassistant/components/izone/manifest.json index 1db99b1a0e3..479ac496906 100644 --- a/homeassistant/components/izone/manifest.json +++ b/homeassistant/components/izone/manifest.json @@ -2,7 +2,12 @@ "domain": "izone", "name": "iZone", "documentation": "https://www.home-assistant.io/integrations/izone", - "requirements": ["python-izone==1.1.2"], + "requirements": ["python-izone==1.1.3"], "codeowners": ["@Swamp-Ig"], - "config_flow": true + "config_flow": true, + "homekit": { + "models": [ + "iZone" + ] + } } diff --git a/homeassistant/components/izone/translations/de.json b/homeassistant/components/izone/translations/de.json index ea59cc39b27..f6e03c3af27 100644 --- a/homeassistant/components/izone/translations/de.json +++ b/homeassistant/components/izone/translations/de.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Es wurden keine iZone-Ger\u00e4te im Netzwerk gefunden.", - "single_instance_allowed": "Es ist nur eine einzige Konfiguration von iZone erforderlich." + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "step": { "confirm": { diff --git a/homeassistant/components/izone/translations/tr.json b/homeassistant/components/izone/translations/tr.json new file mode 100644 index 00000000000..faa20ed0ece --- /dev/null +++ b/homeassistant/components/izone/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "confirm": { + "description": "\u0130Zone'u kurmak istiyor musunuz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/translations/uk.json b/homeassistant/components/izone/translations/uk.json new file mode 100644 index 00000000000..8ab6c1e1664 --- /dev/null +++ b/homeassistant/components/izone/translations/uk.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "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." + }, + "step": { + "confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 iZone?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/juicenet/translations/de.json b/homeassistant/components/juicenet/translations/de.json index 16f48ef3837..7a6b5cff541 100644 --- a/homeassistant/components/juicenet/translations/de.json +++ b/homeassistant/components/juicenet/translations/de.json @@ -1,20 +1,20 @@ { "config": { "abort": { - "already_configured": "Dieses JuiceNet-Konto ist bereits konfiguriert" + "already_configured": "Konto wurde bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { "user": { "data": { - "api_token": "JuiceNet API Token" + "api_token": "API-Token" }, - "description": "Sie ben\u00f6tigen das API-Token von https://home.juice.net/Manage.", - "title": "Stellen Sie eine Verbindung zu JuiceNet her" + "description": "Du ben\u00f6tigst das API-Token von https://home.juice.net/Manage.", + "title": "Stelle eine Verbindung zu JuiceNet her" } } } diff --git a/homeassistant/components/juicenet/translations/tr.json b/homeassistant/components/juicenet/translations/tr.json new file mode 100644 index 00000000000..53890eb41e2 --- /dev/null +++ b/homeassistant/components/juicenet/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "api_token": "API Belirteci" + }, + "description": "API Belirtecine https://home.juice.net/Manage adresinden ihtiyac\u0131n\u0131z olacak.", + "title": "JuiceNet'e ba\u011flan\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/juicenet/translations/uk.json b/homeassistant/components/juicenet/translations/uk.json new file mode 100644 index 00000000000..903ea5f6e74 --- /dev/null +++ b/homeassistant/components/juicenet/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "api_token": "\u0422\u043e\u043a\u0435\u043d API" + }, + "description": "\u0414\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u043e\u0442\u0440\u0456\u0431\u0435\u043d \u0442\u043e\u043a\u0435\u043d API \u0437 \u0441\u0430\u0439\u0442\u0443 https://home.juice.net/Manage.", + "title": "JuiceNet" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index 31230315954..7dbeb513e09 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -4,6 +4,7 @@ import logging import voluptuous as vol from xknx import XKNX +from xknx.core.telegram_queue import TelegramQueue from xknx.devices import DateTime, ExposeSensor from xknx.dpt import DPTArray, DPTBase, DPTBinary from xknx.exceptions import XKNXException @@ -14,6 +15,7 @@ from xknx.io import ( ConnectionType, ) from xknx.telegram import AddressFilter, GroupAddress, Telegram +from xknx.telegram.apci import GroupValueResponse, GroupValueWrite from homeassistant.const import ( CONF_ENTITY_ID, @@ -58,7 +60,7 @@ CONF_KNX_CONFIG = "config_file" CONF_KNX_ROUTING = "routing" CONF_KNX_TUNNELING = "tunneling" CONF_KNX_FIRE_EVENT = "fire_event" -CONF_KNX_FIRE_EVENT_FILTER = "fire_event_filter" +CONF_KNX_EVENT_FILTER = "event_filter" CONF_KNX_INDIVIDUAL_ADDRESS = "individual_address" CONF_KNX_MCAST_GRP = "multicast_group" CONF_KNX_MCAST_PORT = "multicast_port" @@ -70,62 +72,72 @@ SERVICE_KNX_SEND = "send" SERVICE_KNX_ATTR_ADDRESS = "address" SERVICE_KNX_ATTR_PAYLOAD = "payload" SERVICE_KNX_ATTR_TYPE = "type" +SERVICE_KNX_ATTR_REMOVE = "remove" +SERVICE_KNX_EVENT_REGISTER = "event_register" CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_KNX_CONFIG): cv.string, - vol.Exclusive( - CONF_KNX_ROUTING, "connection_type" - ): ConnectionSchema.ROUTING_SCHEMA, - vol.Exclusive( - CONF_KNX_TUNNELING, "connection_type" - ): ConnectionSchema.TUNNELING_SCHEMA, - vol.Inclusive(CONF_KNX_FIRE_EVENT, "fire_ev"): cv.boolean, - vol.Inclusive(CONF_KNX_FIRE_EVENT_FILTER, "fire_ev"): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Optional( - CONF_KNX_INDIVIDUAL_ADDRESS, default=XKNX.DEFAULT_ADDRESS - ): cv.string, - vol.Optional(CONF_KNX_MCAST_GRP, default=DEFAULT_MCAST_GRP): cv.string, - vol.Optional(CONF_KNX_MCAST_PORT, default=DEFAULT_MCAST_PORT): cv.port, - vol.Optional(CONF_KNX_STATE_UPDATER, default=True): cv.boolean, - vol.Optional(CONF_KNX_RATE_LIMIT, default=20): vol.All( - vol.Coerce(int), vol.Range(min=1, max=100) - ), - vol.Optional(CONF_KNX_EXPOSE): vol.All( - cv.ensure_list, [ExposeSchema.SCHEMA] - ), - vol.Optional(SupportedPlatforms.cover.value): vol.All( - cv.ensure_list, [CoverSchema.SCHEMA] - ), - vol.Optional(SupportedPlatforms.binary_sensor.value): vol.All( - cv.ensure_list, [BinarySensorSchema.SCHEMA] - ), - vol.Optional(SupportedPlatforms.light.value): vol.All( - cv.ensure_list, [LightSchema.SCHEMA] - ), - vol.Optional(SupportedPlatforms.climate.value): vol.All( - cv.ensure_list, [ClimateSchema.SCHEMA] - ), - vol.Optional(SupportedPlatforms.notify.value): vol.All( - cv.ensure_list, [NotifySchema.SCHEMA] - ), - vol.Optional(SupportedPlatforms.switch.value): vol.All( - cv.ensure_list, [SwitchSchema.SCHEMA] - ), - vol.Optional(SupportedPlatforms.sensor.value): vol.All( - cv.ensure_list, [SensorSchema.SCHEMA] - ), - vol.Optional(SupportedPlatforms.scene.value): vol.All( - cv.ensure_list, [SceneSchema.SCHEMA] - ), - vol.Optional(SupportedPlatforms.weather.value): vol.All( - cv.ensure_list, [WeatherSchema.SCHEMA] - ), - } + DOMAIN: vol.All( + cv.deprecated(CONF_KNX_FIRE_EVENT), + cv.deprecated("fire_event_filter", replacement_key=CONF_KNX_EVENT_FILTER), + vol.Schema( + { + vol.Optional(CONF_KNX_CONFIG): cv.string, + vol.Exclusive( + CONF_KNX_ROUTING, "connection_type" + ): ConnectionSchema.ROUTING_SCHEMA, + vol.Exclusive( + CONF_KNX_TUNNELING, "connection_type" + ): ConnectionSchema.TUNNELING_SCHEMA, + vol.Optional(CONF_KNX_FIRE_EVENT): cv.boolean, + vol.Optional(CONF_KNX_EVENT_FILTER, default=[]): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional( + CONF_KNX_INDIVIDUAL_ADDRESS, default=XKNX.DEFAULT_ADDRESS + ): cv.string, + vol.Optional( + CONF_KNX_MCAST_GRP, default=DEFAULT_MCAST_GRP + ): cv.string, + vol.Optional( + CONF_KNX_MCAST_PORT, default=DEFAULT_MCAST_PORT + ): cv.port, + vol.Optional(CONF_KNX_STATE_UPDATER, default=True): cv.boolean, + vol.Optional(CONF_KNX_RATE_LIMIT, default=20): vol.All( + vol.Coerce(int), vol.Range(min=1, max=100) + ), + vol.Optional(CONF_KNX_EXPOSE): vol.All( + cv.ensure_list, [ExposeSchema.SCHEMA] + ), + vol.Optional(SupportedPlatforms.cover.value): vol.All( + cv.ensure_list, [CoverSchema.SCHEMA] + ), + vol.Optional(SupportedPlatforms.binary_sensor.value): vol.All( + cv.ensure_list, [BinarySensorSchema.SCHEMA] + ), + vol.Optional(SupportedPlatforms.light.value): vol.All( + cv.ensure_list, [LightSchema.SCHEMA] + ), + vol.Optional(SupportedPlatforms.climate.value): vol.All( + cv.ensure_list, [ClimateSchema.SCHEMA] + ), + vol.Optional(SupportedPlatforms.notify.value): vol.All( + cv.ensure_list, [NotifySchema.SCHEMA] + ), + vol.Optional(SupportedPlatforms.switch.value): vol.All( + cv.ensure_list, [SwitchSchema.SCHEMA] + ), + vol.Optional(SupportedPlatforms.sensor.value): vol.All( + cv.ensure_list, [SensorSchema.SCHEMA] + ), + vol.Optional(SupportedPlatforms.scene.value): vol.All( + cv.ensure_list, [SceneSchema.SCHEMA] + ), + vol.Optional(SupportedPlatforms.weather.value): vol.All( + cv.ensure_list, [WeatherSchema.SCHEMA] + ), + } + ), ) }, extra=vol.ALLOW_EXTRA, @@ -150,6 +162,13 @@ SERVICE_KNX_SEND_SCHEMA = vol.Any( ), ) +SERVICE_KNX_EVENT_REGISTER_SCHEMA = vol.Schema( + { + vol.Required(SERVICE_KNX_ATTR_ADDRESS): cv.string, + vol.Optional(SERVICE_KNX_ATTR_REMOVE, default=False): cv.boolean, + } +) + async def async_setup(hass, config): """Set up the KNX component.""" @@ -187,6 +206,14 @@ async def async_setup(hass, config): schema=SERVICE_KNX_SEND_SCHEMA, ) + async_register_admin_service( + hass, + DOMAIN, + SERVICE_KNX_EVENT_REGISTER, + hass.data[DOMAIN].service_event_register_modify, + schema=SERVICE_KNX_EVENT_REGISTER_SCHEMA, + ) + async def reload_service_handler(service_call: ServiceCallType) -> None: """Remove all KNX components and load new ones from config.""" @@ -220,10 +247,11 @@ class KNXModule: self.hass = hass self.config = config self.connected = False - self.init_xknx() - self.register_callbacks() self.exposures = [] + self.init_xknx() + self._knx_event_callback: TelegramQueue.Callback = self.register_callback() + def init_xknx(self): """Initialize of KNX object.""" self.xknx = XKNX( @@ -288,19 +316,6 @@ class KNXModule: auto_reconnect=True, ) - def register_callbacks(self): - """Register callbacks within XKNX object.""" - if ( - CONF_KNX_FIRE_EVENT in self.config[DOMAIN] - and self.config[DOMAIN][CONF_KNX_FIRE_EVENT] - ): - address_filters = list( - map(AddressFilter, self.config[DOMAIN][CONF_KNX_FIRE_EVENT_FILTER]) - ) - self.xknx.telegram_queue.register_telegram_received_cb( - self.telegram_received_cb, address_filters - ) - @callback def async_create_exposures(self): """Create exposures.""" @@ -331,11 +346,52 @@ class KNXModule: async def telegram_received_cb(self, telegram): """Call invoked after a KNX telegram was received.""" + data = None + + # Not all telegrams have serializable data. + if isinstance(telegram.payload, (GroupValueWrite, GroupValueResponse)): + data = telegram.payload.value.value + self.hass.bus.async_fire( "knx_event", - {"address": str(telegram.group_address), "data": telegram.payload.value}, + { + "data": data, + "destination": str(telegram.destination_address), + "direction": telegram.direction.value, + "source": str(telegram.source_address), + "telegramtype": telegram.payload.__class__.__name__, + }, ) + def register_callback(self) -> TelegramQueue.Callback: + """Register callback within XKNX TelegramQueue.""" + address_filters = list( + map(AddressFilter, self.config[DOMAIN][CONF_KNX_EVENT_FILTER]) + ) + return self.xknx.telegram_queue.register_telegram_received_cb( + self.telegram_received_cb, + address_filters=address_filters, + group_addresses=[], + ) + + async def service_event_register_modify(self, call): + """Service for adding or removing a GroupAddress to the knx_event filter.""" + group_address = GroupAddress(call.data.get(SERVICE_KNX_ATTR_ADDRESS)) + if call.data.get(SERVICE_KNX_ATTR_REMOVE): + try: + self._knx_event_callback.group_addresses.remove(group_address) + except ValueError: + _LOGGER.warning( + "Service event_register could not remove event for '%s'", + group_address, + ) + elif group_address not in self._knx_event_callback.group_addresses: + self._knx_event_callback.group_addresses.append(group_address) + _LOGGER.debug( + "Service event_register registered event for '%s'", + group_address, + ) + async def service_send_to_knx_bus(self, call): """Service for sending an arbitrary KNX message to the KNX bus.""" attr_payload = call.data.get(SERVICE_KNX_ATTR_PAYLOAD) @@ -353,10 +409,10 @@ class KNXModule: return DPTBinary(attr_payload) return DPTArray(attr_payload) - payload = calculate_payload(attr_payload) - address = GroupAddress(attr_address) - - telegram = Telegram(group_address=address, payload=payload) + telegram = Telegram( + destination_address=GroupAddress(attr_address), + payload=GroupValueWrite(calculate_payload(attr_payload)), + ) await self.xknx.telegrams.put(telegram) diff --git a/homeassistant/components/knx/climate.py b/homeassistant/components/knx/climate.py index 070f635cc48..565c41298a3 100644 --- a/homeassistant/components/knx/climate.py +++ b/homeassistant/components/knx/climate.py @@ -2,7 +2,7 @@ from typing import List, Optional from xknx.devices import Climate as XknxClimate -from xknx.dpt import HVACControllerMode, HVACOperationMode +from xknx.dpt.dpt_hvac_mode import HVACControllerMode, HVACOperationMode from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( diff --git a/homeassistant/components/knx/factory.py b/homeassistant/components/knx/factory.py index 385b7c009ed..c1e73733b22 100644 --- a/homeassistant/components/knx/factory.py +++ b/homeassistant/components/knx/factory.py @@ -1,4 +1,6 @@ """Factory function to initialize KNX devices from config.""" +from typing import Optional, Tuple + from xknx import XKNX from xknx.devices import ( BinarySensor as XknxBinarySensor, @@ -86,8 +88,30 @@ def _create_cover(knx_module: XKNX, config: ConfigType) -> XknxCover: ) +def _create_light_color( + color: str, config: ConfigType +) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[str]]: + """Load color configuration from configuration structure.""" + if "individual_colors" in config and color in config["individual_colors"]: + sub_config = config["individual_colors"][color] + group_address_switch = sub_config.get(CONF_ADDRESS) + group_address_switch_state = sub_config.get(LightSchema.CONF_STATE_ADDRESS) + group_address_brightness = sub_config.get(LightSchema.CONF_BRIGHTNESS_ADDRESS) + group_address_brightness_state = sub_config.get( + LightSchema.CONF_BRIGHTNESS_STATE_ADDRESS + ) + return ( + group_address_switch, + group_address_switch_state, + group_address_brightness, + group_address_brightness_state, + ) + return None, None, None, None + + def _create_light(knx_module: XKNX, config: ConfigType) -> XknxLight: """Return a KNX Light device to be used within XKNX.""" + group_address_tunable_white = None group_address_tunable_white_state = None group_address_color_temp = None @@ -103,10 +127,35 @@ def _create_light(knx_module: XKNX, config: ConfigType) -> XknxLight: LightSchema.CONF_COLOR_TEMP_STATE_ADDRESS ) + ( + red_switch, + red_switch_state, + red_brightness, + red_brightness_state, + ) = _create_light_color(LightSchema.CONF_RED, config) + ( + green_switch, + green_switch_state, + green_brightness, + green_brightness_state, + ) = _create_light_color(LightSchema.CONF_GREEN, config) + ( + blue_switch, + blue_switch_state, + blue_brightness, + blue_brightness_state, + ) = _create_light_color(LightSchema.CONF_BLUE, config) + ( + white_switch, + white_switch_state, + white_brightness, + white_brightness_state, + ) = _create_light_color(LightSchema.CONF_WHITE, config) + return XknxLight( knx_module, name=config[CONF_NAME], - group_address_switch=config[CONF_ADDRESS], + group_address_switch=config.get(CONF_ADDRESS), group_address_switch_state=config.get(LightSchema.CONF_STATE_ADDRESS), group_address_brightness=config.get(LightSchema.CONF_BRIGHTNESS_ADDRESS), group_address_brightness_state=config.get( @@ -120,6 +169,22 @@ def _create_light(knx_module: XKNX, config: ConfigType) -> XknxLight: group_address_tunable_white_state=group_address_tunable_white_state, group_address_color_temperature=group_address_color_temp, group_address_color_temperature_state=group_address_color_temp_state, + group_address_switch_red=red_switch, + group_address_switch_red_state=red_switch_state, + group_address_brightness_red=red_brightness, + group_address_brightness_red_state=red_brightness_state, + group_address_switch_green=green_switch, + group_address_switch_green_state=green_switch_state, + group_address_brightness_green=green_brightness, + group_address_brightness_green_state=green_brightness_state, + group_address_switch_blue=blue_switch, + group_address_switch_blue_state=blue_switch_state, + group_address_brightness_blue=blue_brightness, + group_address_brightness_blue_state=blue_brightness_state, + group_address_switch_white=white_switch, + group_address_switch_white_state=white_switch_state, + group_address_brightness_white=white_brightness, + group_address_brightness_white_state=white_brightness_state, min_kelvin=config[LightSchema.CONF_MIN_KELVIN], max_kelvin=config[LightSchema.CONF_MAX_KELVIN], ) diff --git a/homeassistant/components/knx/knx_entity.py b/homeassistant/components/knx/knx_entity.py index 296bcb2f540..f4597ad230e 100644 --- a/homeassistant/components/knx/knx_entity.py +++ b/homeassistant/components/knx/knx_entity.py @@ -40,12 +40,12 @@ class KnxEntity(Entity): """Store register state change callback.""" self._device.register_device_updated_cb(self.after_update_callback) - if isinstance(self._device, XknxClimate): + if isinstance(self._device, XknxClimate) and self._device.mode is not None: self._device.mode.register_device_updated_cb(self.after_update_callback) async def async_will_remove_from_hass(self) -> None: """Disconnect device object when removed.""" self._device.unregister_device_updated_cb(self.after_update_callback) - if isinstance(self._device, XknxClimate): + if isinstance(self._device, XknxClimate) and self._device.mode is not None: self._device.mode.unregister_device_updated_cb(self.after_update_callback) diff --git a/homeassistant/components/knx/manifest.json b/homeassistant/components/knx/manifest.json index 631a6329c8c..93daee0e348 100644 --- a/homeassistant/components/knx/manifest.json +++ b/homeassistant/components/knx/manifest.json @@ -2,7 +2,7 @@ "domain": "knx", "name": "KNX", "documentation": "https://www.home-assistant.io/integrations/knx", - "requirements": ["xknx==0.15.6"], + "requirements": ["xknx==0.16.2"], "codeowners": ["@Julius2342", "@farmio", "@marvin-w"], "quality_scale": "silver" } diff --git a/homeassistant/components/knx/schema.py b/homeassistant/components/knx/schema.py index b1d791e3284..22599014d0f 100644 --- a/homeassistant/components/knx/schema.py +++ b/homeassistant/components/knx/schema.py @@ -139,31 +139,73 @@ class LightSchema: DEFAULT_MIN_KELVIN = 2700 # 370 mireds DEFAULT_MAX_KELVIN = 6000 # 166 mireds - SCHEMA = vol.Schema( + CONF_INDIVIDUAL_COLORS = "individual_colors" + CONF_RED = "red" + CONF_GREEN = "green" + CONF_BLUE = "blue" + CONF_WHITE = "white" + + COLOR_SCHEMA = vol.Schema( { - vol.Required(CONF_ADDRESS): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_ADDRESS): cv.string, vol.Optional(CONF_STATE_ADDRESS): cv.string, - vol.Optional(CONF_BRIGHTNESS_ADDRESS): cv.string, + vol.Required(CONF_BRIGHTNESS_ADDRESS): cv.string, vol.Optional(CONF_BRIGHTNESS_STATE_ADDRESS): cv.string, - vol.Optional(CONF_COLOR_ADDRESS): cv.string, - vol.Optional(CONF_COLOR_STATE_ADDRESS): cv.string, - vol.Optional(CONF_COLOR_TEMP_ADDRESS): cv.string, - vol.Optional(CONF_COLOR_TEMP_STATE_ADDRESS): cv.string, - vol.Optional( - CONF_COLOR_TEMP_MODE, default=DEFAULT_COLOR_TEMP_MODE - ): cv.enum(ColorTempModes), - vol.Optional(CONF_RGBW_ADDRESS): cv.string, - vol.Optional(CONF_RGBW_STATE_ADDRESS): cv.string, - vol.Optional(CONF_MIN_KELVIN, default=DEFAULT_MIN_KELVIN): vol.All( - vol.Coerce(int), vol.Range(min=1) - ), - vol.Optional(CONF_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): vol.All( - vol.Coerce(int), vol.Range(min=1) - ), } ) + SCHEMA = vol.All( + vol.Schema( + { + vol.Optional(CONF_ADDRESS): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_STATE_ADDRESS): cv.string, + vol.Optional(CONF_BRIGHTNESS_ADDRESS): cv.string, + vol.Optional(CONF_BRIGHTNESS_STATE_ADDRESS): cv.string, + vol.Exclusive(CONF_INDIVIDUAL_COLORS, "color"): { + vol.Inclusive(CONF_RED, "colors"): COLOR_SCHEMA, + vol.Inclusive(CONF_GREEN, "colors"): COLOR_SCHEMA, + vol.Inclusive(CONF_BLUE, "colors"): COLOR_SCHEMA, + vol.Optional(CONF_WHITE): COLOR_SCHEMA, + }, + vol.Exclusive(CONF_COLOR_ADDRESS, "color"): cv.string, + vol.Optional(CONF_COLOR_STATE_ADDRESS): cv.string, + vol.Optional(CONF_COLOR_TEMP_ADDRESS): cv.string, + vol.Optional(CONF_COLOR_TEMP_STATE_ADDRESS): cv.string, + vol.Optional( + CONF_COLOR_TEMP_MODE, default=DEFAULT_COLOR_TEMP_MODE + ): cv.enum(ColorTempModes), + vol.Exclusive(CONF_RGBW_ADDRESS, "color"): cv.string, + vol.Optional(CONF_RGBW_STATE_ADDRESS): cv.string, + vol.Optional(CONF_MIN_KELVIN, default=DEFAULT_MIN_KELVIN): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), + vol.Optional(CONF_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): vol.All( + vol.Coerce(int), vol.Range(min=1) + ), + } + ), + vol.Any( + # either global "address" or all addresses for individual colors are required + vol.Schema( + { + vol.Required(CONF_INDIVIDUAL_COLORS): { + vol.Required(CONF_RED): {vol.Required(CONF_ADDRESS): object}, + vol.Required(CONF_GREEN): {vol.Required(CONF_ADDRESS): object}, + vol.Required(CONF_BLUE): {vol.Required(CONF_ADDRESS): object}, + }, + }, + extra=vol.ALLOW_EXTRA, + ), + vol.Schema( + { + vol.Required(CONF_ADDRESS): object, + }, + extra=vol.ALLOW_EXTRA, + ), + ), + ) + class ClimateSchema: """Voluptuous schema for KNX climate devices.""" diff --git a/homeassistant/components/knx/services.yaml b/homeassistant/components/knx/services.yaml index 03d4e69b32c..cab8c100b01 100644 --- a/homeassistant/components/knx/services.yaml +++ b/homeassistant/components/knx/services.yaml @@ -10,3 +10,11 @@ send: type: description: "Optional. If set, the payload will not be sent as raw bytes, but encoded as given DPT. Knx sensor types are valid values (see https://www.home-assistant.io/integrations/sensor.knx)." example: "temperature" +event_register: + description: "Add or remove single group address to knx_event filter for triggering `knx_event`s. Only addresses added with this service can be removed." + fields: + address: + description: "Group address that shall be added or removed." + example: "1/1/0" + remove: + description: "Optional. If `True` the group address will be removed. Defaults to `False`." diff --git a/homeassistant/components/kodi/media_player.py b/homeassistant/components/kodi/media_player.py index dfe3af4b11e..6b849cb711b 100644 --- a/homeassistant/components/kodi/media_player.py +++ b/homeassistant/components/kodi/media_player.py @@ -398,7 +398,7 @@ class KodiEntity(MediaPlayerEntity): version = (await self._kodi.get_application_properties(["version"]))["version"] sw_version = f"{version['major']}.{version['minor']}" dev_reg = await device_registry.async_get_registry(self.hass) - device = dev_reg.async_get_device({(DOMAIN, self.unique_id)}, []) + device = dev_reg.async_get_device({(DOMAIN, self.unique_id)}) dev_reg.async_update_device(device.id, sw_version=sw_version) self.async_schedule_update_ha_state(True) diff --git a/homeassistant/components/kodi/translations/de.json b/homeassistant/components/kodi/translations/de.json index a0bf05cb5ec..1d229e5a428 100644 --- a/homeassistant/components/kodi/translations/de.json +++ b/homeassistant/components/kodi/translations/de.json @@ -1,10 +1,14 @@ { "config": { "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "flow_title": "Kodi: {name}", diff --git a/homeassistant/components/kodi/translations/tr.json b/homeassistant/components/kodi/translations/tr.json new file mode 100644 index 00000000000..54ad8e0b6fd --- /dev/null +++ b/homeassistant/components/kodi/translations/tr.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "credentials": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "L\u00fctfen Kodi kullan\u0131c\u0131 ad\u0131n\u0131z\u0131 ve \u015fifrenizi girin. Bunlar Sistem / Ayarlar / A\u011f / Hizmetler'de bulunabilir." + }, + "user": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + } + }, + "ws_port": { + "data": { + "ws_port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kodi/translations/uk.json b/homeassistant/components/kodi/translations/uk.json new file mode 100644 index 00000000000..d2acde5dffb --- /dev/null +++ b/homeassistant/components/kodi/translations/uk.json @@ -0,0 +1,50 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "no_uuid": "\u0423 \u0434\u0430\u043d\u043e\u0433\u043e \u0435\u043a\u0437\u0435\u043c\u043f\u043b\u044f\u0440\u0430 Kodi \u043d\u0435\u043c\u0430\u0454 \u0443\u043d\u0456\u043a\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0456\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440\u0430. \u0406\u043c\u043e\u0432\u0456\u0440\u043d\u043e, \u0446\u0435 \u043f\u043e\u0432'\u044f\u0437\u0430\u043d\u043e \u0437 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f\u043c \u0441\u0442\u0430\u0440\u043e\u0457 \u0432\u0435\u0440\u0441\u0456\u0457 Kodi (17.x \u0430\u0431\u043e \u043d\u0438\u0436\u0447\u0435). \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e \u0432\u0440\u0443\u0447\u043d\u0443 \u0430\u0431\u043e \u043f\u0435\u0440\u0435\u0439\u0442\u0438 \u043d\u0430 \u043d\u043e\u0432\u0456\u0448\u0443 \u0432\u0435\u0440\u0441\u0456\u044e Kodi.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "flow_title": "Kodi: {name}", + "step": { + "credentials": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430 \u0456 \u043f\u0430\u0440\u043e\u043b\u044c Kodi. \u0407\u0445 \u043c\u043e\u0436\u043d\u0430 \u0437\u043d\u0430\u0439\u0442\u0438, \u043f\u0435\u0440\u0435\u0439\u0448\u043e\u0432\u0448\u0438 \u0432 \"\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\" - \"\u0421\u043b\u0443\u0436\u0431\u0438\" - \"\u0423\u043f\u0440\u0430\u0432\u043b\u0456\u043d\u043d\u044f\"." + }, + "discovery_confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 Kodi (`{name}`)?", + "title": "Kodi" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL" + }, + "description": "\u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e \"\u0414\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0438 \u0432\u0456\u0434\u0434\u0430\u043b\u0435\u043d\u0435 \u0443\u043f\u0440\u0430\u0432\u043b\u0456\u043d\u043d\u044f \u043f\u043e HTTP\" \u0432 \u0440\u043e\u0437\u0434\u0456\u043b\u0456 \"\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\" - \"\u0421\u043b\u0443\u0436\u0431\u0438\" - \"\u0423\u043f\u0440\u0430\u0432\u043b\u0456\u043d\u043d\u044f\"." + }, + "ws_port": { + "data": { + "ws_port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u043f\u043e WebSocket. \u0429\u043e\u0431 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f \u0447\u0435\u0440\u0435\u0437 WebSocket, \u0412\u0430\u043c \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0443\u043f\u0440\u0430\u0432\u043b\u0456\u043d\u043d\u044f \u0434\u043e\u0434\u0430\u0442\u043a\u0430\u043c\u0438 \u0432 \u0440\u043e\u0437\u0434\u0456\u043b\u0456 \"\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\" - \"\u0421\u043b\u0443\u0436\u0431\u0438\" - \"\u0423\u043f\u0440\u0430\u0432\u043b\u0456\u043d\u043d\u044f\". \u042f\u043a\u0449\u043e WebSocket \u043d\u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439, \u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u043b\u0435 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c." + } + } + }, + "device_automation": { + "trigger_type": { + "turn_off": "\u0437\u0430\u043f\u0438\u0442\u0430\u043d\u043e \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043d\u044f {entity_name}", + "turn_on": "\u0437\u0430\u043f\u0438\u0442\u0430\u043d\u043e \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043d\u044f {entity_name}" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/de.json b/homeassistant/components/konnected/translations/de.json index ad2ed659522..2ec1657990b 100644 --- a/homeassistant/components/konnected/translations/de.json +++ b/homeassistant/components/konnected/translations/de.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "already_in_progress": "Der Konfigurationsfluss f\u00fcr das Ger\u00e4t wird bereits ausgef\u00fchrt.", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "not_konn_panel": "Kein anerkanntes Konnected.io-Ger\u00e4t", - "unknown": "Unbekannter Fehler ist aufgetreten" + "unknown": "Unerwarteter Fehler" }, "error": { "cannot_connect": "Es konnte keine Verbindung zu einem Konnected-Panel unter {host}:{port} hergestellt werden." @@ -43,7 +43,7 @@ "name": "Name (optional)", "type": "Bin\u00e4rer Sensortyp" }, - "description": "Bitte w\u00e4hlen Sie die Optionen f\u00fcr den an {zone} angeschlossenen Bin\u00e4rsensor", + "description": "Bitte w\u00e4hle die Optionen f\u00fcr den an {zone} angeschlossenen Bin\u00e4rsensor", "title": "Konfigurieren Sie den Bin\u00e4rsensor" }, "options_digital": { @@ -52,7 +52,7 @@ "poll_interval": "Abfrageintervall (Minuten) (optional)", "type": "Sensortyp" }, - "description": "Bitte w\u00e4hlen Sie die Optionen f\u00fcr den an {zone} angeschlossenen digitalen Sensor aus", + "description": "Bitte w\u00e4hle die Optionen f\u00fcr den an {zone} angeschlossenen digitalen Sensor aus", "title": "Konfigurieren Sie den digitalen Sensor" }, "options_io": { @@ -98,9 +98,9 @@ "more_states": "Konfigurieren Sie zus\u00e4tzliche Zust\u00e4nde f\u00fcr diese Zone", "name": "Name (optional)", "pause": "Pause zwischen Impulsen (ms) (optional)", - "repeat": "Zeit zum Wiederholen (-1 = unendlich) (optional)" + "repeat": "Mal wiederholen (-1 = unendlich) (optional)" }, - "description": "Bitte w\u00e4hlen Sie die Ausgabeoptionen f\u00fcr {zone} : Status {state}", + "description": "Bitte w\u00e4hlen die Ausgabeoptionen f\u00fcr {zone} : Status {state}", "title": "Konfigurieren Sie den schaltbaren Ausgang" } } diff --git a/homeassistant/components/konnected/translations/pl.json b/homeassistant/components/konnected/translations/pl.json index ee6c10cbdd8..f6e9a2dbfbc 100644 --- a/homeassistant/components/konnected/translations/pl.json +++ b/homeassistant/components/konnected/translations/pl.json @@ -45,7 +45,7 @@ "name": "Nazwa (opcjonalnie)", "type": "Typ sensora binarnego" }, - "description": "Wybierz opcje dla sensora binarnego powi\u0105zanego ze {zone}", + "description": "Opcje {zone}", "title": "Konfiguracja sensora binarnego" }, "options_digital": { @@ -54,7 +54,7 @@ "poll_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji (minuty) (opcjonalnie)", "type": "Typ sensora" }, - "description": "Wybierz opcje dla cyfrowego sensora powi\u0105zanego ze {zone}", + "description": "Opcje {zone}", "title": "Konfiguracja sensora cyfrowego" }, "options_io": { diff --git a/homeassistant/components/konnected/translations/tr.json b/homeassistant/components/konnected/translations/tr.json new file mode 100644 index 00000000000..a0e759903bd --- /dev/null +++ b/homeassistant/components/konnected/translations/tr.json @@ -0,0 +1,60 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "unknown": "Beklenmeyen hata" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "\u0130p Adresi", + "port": "Port" + } + } + } + }, + "options": { + "error": { + "bad_host": "Ge\u00e7ersiz, Ge\u00e7ersiz K\u0131lma API ana makine url'si" + }, + "step": { + "options_binary": { + "data": { + "inverse": "A\u00e7\u0131k / kapal\u0131 durumunu tersine \u00e7evirin" + } + }, + "options_io": { + "data": { + "3": "B\u00f6lge 3", + "4": "B\u00f6lge 4", + "5": "B\u00f6lge 5", + "6": "B\u00f6lge 6", + "7": "B\u00f6lge 7", + "out": "OUT" + } + }, + "options_io_ext": { + "data": { + "10": "B\u00f6lge 10", + "11": "B\u00f6lge 11", + "12": "B\u00f6lge 12", + "8": "B\u00f6lge 8", + "9": "B\u00f6lge 9", + "alarm1": "ALARM1", + "alarm2_out2": "OUT2/ALARM2", + "out1": "OUT1" + } + }, + "options_misc": { + "data": { + "api_host": "API ana makine URL'sini ge\u00e7ersiz k\u0131l (iste\u011fe ba\u011fl\u0131)", + "override_api_host": "Varsay\u0131lan Home Assistant API ana bilgisayar paneli URL'sini ge\u00e7ersiz k\u0131l" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/uk.json b/homeassistant/components/konnected/translations/uk.json new file mode 100644 index 00000000000..92cd3744d94 --- /dev/null +++ b/homeassistant/components/konnected/translations/uk.json @@ -0,0 +1,108 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "not_konn_panel": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 Konnected.io \u043d\u0435 \u0440\u043e\u0437\u043f\u0456\u0437\u043d\u0430\u043d\u043e.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "confirm": { + "description": "\u041c\u043e\u0434\u0435\u043b\u044c: {model}\nID: {id}\n\u0425\u043e\u0441\u0442: {host}\n\u041f\u043e\u0440\u0442: {port} \n\n\u0417\u043c\u0456\u043d\u0430 \u043b\u043e\u0433\u0456\u043a\u0438 \u0440\u043e\u0431\u043e\u0442\u0438 \u043f\u0430\u043d\u0435\u043b\u0456, \u0430 \u0442\u0430\u043a\u043e\u0436 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044f \u0432\u0445\u043e\u0434\u0456\u0432 \u0456 \u0432\u0438\u0445\u043e\u0434\u0456\u0432 \u0432\u0438\u043a\u043e\u043d\u0443\u0454\u0442\u044c\u0441\u044f \u0432 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u0445 \u043f\u0430\u043d\u0435\u043b\u0456 \u0441\u0438\u0433\u043d\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u0457 Konnected.", + "title": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 Konnected \u0433\u043e\u0442\u043e\u0432\u0456\u0439 \u0434\u043e \u0440\u043e\u0431\u043e\u0442\u0438." + }, + "import_confirm": { + "description": "\u041f\u0430\u043d\u0435\u043b\u044c \u0441\u0438\u0433\u043d\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u0457 Konnected ID {id} \u0440\u0430\u043d\u0456\u0448\u0435 \u0432\u0436\u0435 \u0431\u0443\u043b\u0430 \u0434\u043e\u0434\u0430\u043d\u0430 \u0447\u0435\u0440\u0435\u0437 configuration.yaml. \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0456\u043c\u043f\u043e\u0440\u0442\u0443\u0432\u0430\u0442\u0438 \u0446\u0435\u0439 \u0437\u0430\u043f\u0438\u0441 \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u0434\u0430\u043d\u043e\u0433\u043e \u043f\u043e\u0441\u0456\u0431\u043d\u0438\u043a\u0430 \u0437 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f.", + "title": "\u0406\u043c\u043f\u043e\u0440\u0442 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Konnected" + }, + "user": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0430\u043d\u0435\u043b\u0456 Konnected." + } + } + }, + "options": { + "abort": { + "not_konn_panel": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 Konnected.io \u043d\u0435 \u0440\u043e\u0437\u043f\u0456\u0437\u043d\u0430\u043d\u043e." + }, + "error": { + "bad_host": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 URL \u043f\u0435\u0440\u0435\u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0445\u043e\u0441\u0442\u0430 API." + }, + "step": { + "options_binary": { + "data": { + "inverse": "\u0406\u043d\u0432\u0435\u0440\u0442\u0443\u0432\u0430\u0442\u0438 \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u0438\u0439/\u0437\u0430\u043a\u0440\u0438\u0442\u0438\u0439 \u0441\u0442\u0430\u043d", + "name": "\u041d\u0430\u0437\u0432\u0430 (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)", + "type": "\u0422\u0438\u043f \u0431\u0456\u043d\u0430\u0440\u043d\u043e\u0433\u043e \u0441\u0435\u043d\u0441\u043e\u0440\u0430" + }, + "description": "\u041e\u043f\u0446\u0456\u0457 {zone}", + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0431\u0456\u043d\u0430\u0440\u043d\u043e\u0433\u043e \u0441\u0435\u043d\u0441\u043e\u0440\u0430" + }, + "options_digital": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430 (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)", + "poll_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0438\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432 \u0445\u0432\u0438\u043b\u0438\u043d\u0430\u0445 (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)", + "type": "\u0422\u0438\u043f \u0441\u0435\u043d\u0441\u043e\u0440\u0430" + }, + "description": "\u041e\u043f\u0446\u0456\u0457 {zone}", + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0430" + }, + "options_io": { + "data": { + "1": "\u0417\u043e\u043d\u0430 1", + "2": "\u0417\u043e\u043d\u0430 2", + "3": "\u0417\u043e\u043d\u0430 3", + "4": "\u0417\u043e\u043d\u0430 4", + "5": "\u0417\u043e\u043d\u0430 5", + "6": "\u0417\u043e\u043d\u0430 6", + "7": "\u0417\u043e\u043d\u0430 7", + "out": "\u0412\u0418\u0425\u0406\u0414" + }, + "description": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 {model} \u0437 \u0430\u0434\u0440\u0435\u0441\u043e\u044e {host}. \u0417\u0430\u043b\u0435\u0436\u043d\u043e \u0432\u0456\u0434 \u043e\u0431\u0440\u0430\u043d\u043e\u0457 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457 \u0432\u0445\u043e\u0434\u0456\u0432 / \u0432\u0438\u0445\u043e\u0434\u0456\u0432, \u0434\u043e \u043f\u0430\u043d\u0435\u043b\u0456 \u043c\u043e\u0436\u0443\u0442\u044c \u0431\u0443\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0456 \u0431\u0456\u043d\u0430\u0440\u043d\u0456 \u0441\u0435\u043d\u0441\u043e\u0440\u0438 (\u0434\u0430\u0442\u0447\u0438\u043a\u0438 \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u0442\u044f / \u0437\u0430\u043a\u0440\u0438\u0442\u0442\u044f), \u0446\u0438\u0444\u0440\u043e\u0432\u0456 \u0441\u0435\u043d\u0441\u043e\u0440\u0438 (dht \u0456 ds18b20) \u0430\u0431\u043e \u043f\u0435\u0440\u0435\u043c\u0438\u043a\u0430\u044e\u0447\u0456 \u0432\u0438\u0445\u043e\u0434\u0438. \u0411\u0456\u043b\u044c\u0448 \u0434\u0435\u0442\u0430\u043b\u044c\u043d\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0431\u0443\u0434\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0435 \u043d\u0430 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0438\u0445 \u043a\u0440\u043e\u043a\u0430\u0445.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0445\u043e\u0434\u0456\u0432 / \u0432\u0438\u0445\u043e\u0434\u0456\u0432" + }, + "options_io_ext": { + "data": { + "10": "\u0417\u043e\u043d\u0430 10", + "11": "\u0417\u043e\u043d\u0430 11", + "12": "\u0417\u043e\u043d\u0430 12", + "8": "\u0417\u043e\u043d\u0430 8", + "9": "\u0417\u043e\u043d\u0430 9", + "alarm1": "\u0422\u0420\u0418\u0412\u041e\u0413\u04101", + "alarm2_out2": "\u0412\u0418\u0425\u0406\u04142 / \u0422\u0420\u0418\u0412\u041e\u0413\u04102", + "out1": "\u0412\u0418\u0425\u0406\u04141" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e \u0440\u0435\u0448\u0442\u0438 \u0432\u0445\u043e\u0434\u0456\u0432 / \u0432\u0438\u0445\u043e\u0434\u0456\u0432. \u0411\u0456\u043b\u044c\u0448 \u0434\u0435\u0442\u0430\u043b\u044c\u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0431\u0443\u0434\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430 \u043d\u0430 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0438\u0445 \u043a\u0440\u043e\u043a\u0430\u0445.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0438\u0445 \u0432\u0445\u043e\u0434\u0456\u0432 / \u0432\u0438\u0445\u043e\u0434\u0456\u0432" + }, + "options_misc": { + "data": { + "api_host": "\u041f\u0435\u0440\u0435\u0432\u0438\u0437\u043d\u0430\u0447\u0438\u0442\u0438 URL \u0445\u043e\u0441\u0442\u0430 API (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)", + "blink": "LED-\u0456\u043d\u0434\u0438\u043a\u0430\u0446\u0456\u044f \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0456 \u043f\u0440\u0438 \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u0446\u0456 \u0441\u0442\u0430\u043d\u0443", + "discovery": "\u0412\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u0430\u0442\u0438 \u043d\u0430 \u0437\u0430\u043f\u0438\u0442\u0438 \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f \u0443 \u0412\u0430\u0448\u0456\u0439 \u043c\u0435\u0440\u0435\u0436\u0456", + "override_api_host": "\u041f\u0435\u0440\u0435\u0432\u0438\u0437\u043d\u0430\u0447\u0438\u0442\u0438 URL-\u0430\u0434\u0440\u0435\u0441\u0443 \u0445\u043e\u0441\u0442-\u043f\u0430\u043d\u0435\u043b\u0456 Home Assistant API" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0431\u0430\u0436\u0430\u043d\u0443 \u043f\u043e\u0432\u0435\u0434\u0456\u043d\u043a\u0443 \u0434\u043b\u044f \u0412\u0430\u0448\u043e\u0457 \u043f\u0430\u043d\u0435\u043b\u0456.", + "title": "\u0406\u043d\u0448\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f" + }, + "options_switch": { + "data": { + "activation": "\u0412\u0438\u0445\u0456\u0434 \u043f\u0440\u0438 \u0432\u043c\u0438\u043a\u0430\u043d\u043d\u0456", + "momentary": "\u0422\u0440\u0438\u0432\u0430\u043b\u0456\u0441\u0442\u044c \u0456\u043c\u043f\u0443\u043b\u044c\u0441\u0443 (\u043c\u0441) (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)", + "more_states": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0456 \u0441\u0442\u0430\u043d\u0438 \u0434\u043b\u044f \u0446\u0456\u0454\u0457 \u0437\u043e\u043d\u0438", + "name": "\u041d\u0430\u0437\u0432\u0430 (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)", + "pause": "\u041f\u0430\u0443\u0437\u0430 \u043c\u0456\u0436 \u0456\u043c\u043f\u0443\u043b\u044c\u0441\u0430\u043c\u0438 (\u043c\u0441) (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)", + "repeat": "\u041a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u0435\u043d\u044c (-1 = \u043d\u0435\u0441\u043a\u0456\u043d\u0447\u0435\u043d\u043d\u043e) (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)" + }, + "description": "{zone}: \u0441\u0442\u0430\u043d {state}", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0435\u0440\u0435\u043c\u0438\u043a\u0430\u044e\u0447\u043e\u0433\u043e \u0432\u0438\u0445\u043e\u0434\u0443" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/__init__.py b/homeassistant/components/kulersky/__init__.py index ff984e2c0d3..9459d44805c 100644 --- a/homeassistant/components/kulersky/__init__.py +++ b/homeassistant/components/kulersky/__init__.py @@ -1,15 +1,11 @@ """Kuler Sky lights integration.""" import asyncio -import voluptuous as vol - from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from .const import DOMAIN -CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) - PLATFORMS = ["light"] diff --git a/homeassistant/components/kulersky/translations/de.json b/homeassistant/components/kulersky/translations/de.json index 3fc69f85947..96ed09a974f 100644 --- a/homeassistant/components/kulersky/translations/de.json +++ b/homeassistant/components/kulersky/translations/de.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Wollen Sie mit der Einrichtung beginnen?" + "description": "M\u00f6chtest du mit der Einrichtung beginnen?" } } } diff --git a/homeassistant/components/kulersky/translations/lb.json b/homeassistant/components/kulersky/translations/lb.json new file mode 100644 index 00000000000..4ea09574c0b --- /dev/null +++ b/homeassistant/components/kulersky/translations/lb.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "Keng Apparater am Netzwierk fonnt", + "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech." + }, + "step": { + "confirm": { + "description": "Soll den Ariichtungs Prozess gestart ginn?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/tr.json b/homeassistant/components/kulersky/translations/tr.json index 49fa9545e94..3df15466f03 100644 --- a/homeassistant/components/kulersky/translations/tr.json +++ b/homeassistant/components/kulersky/translations/tr.json @@ -1,7 +1,13 @@ { "config": { "abort": { - "no_devices_found": "A\u011fda cihaz bulunamad\u0131" + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "confirm": { + "description": "Kuruluma ba\u015flamak ister misiniz?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/kulersky/translations/uk.json b/homeassistant/components/kulersky/translations/uk.json new file mode 100644 index 00000000000..292861e9129 --- /dev/null +++ b/homeassistant/components/kulersky/translations/uk.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "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." + }, + "step": { + "confirm": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lastfm/manifest.json b/homeassistant/components/lastfm/manifest.json index adcd7e8d3d0..9fe3a182844 100644 --- a/homeassistant/components/lastfm/manifest.json +++ b/homeassistant/components/lastfm/manifest.json @@ -2,6 +2,6 @@ "domain": "lastfm", "name": "Last.fm", "documentation": "https://www.home-assistant.io/integrations/lastfm", - "requirements": ["pylast==4.0.0"], + "requirements": ["pylast==4.1.0"], "codeowners": [] } diff --git a/homeassistant/components/lcn/__init__.py b/homeassistant/components/lcn/__init__.py index 72f11b7b005..cc1e47d71fc 100644 --- a/homeassistant/components/lcn/__init__.py +++ b/homeassistant/components/lcn/__init__.py @@ -139,7 +139,8 @@ class LcnEntity(Entity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" - self.device_connection.register_for_inputs(self.input_received) + if not self.device_connection.is_group: + self.device_connection.register_for_inputs(self.input_received) @property def name(self): diff --git a/homeassistant/components/lcn/binary_sensor.py b/homeassistant/components/lcn/binary_sensor.py index 5d712045c93..415668f5924 100644 --- a/homeassistant/components/lcn/binary_sensor.py +++ b/homeassistant/components/lcn/binary_sensor.py @@ -50,9 +50,10 @@ class LcnRegulatorLockSensor(LcnEntity, BinarySensorEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler( - self.setpoint_variable - ) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler( + self.setpoint_variable + ) @property def is_on(self): @@ -85,9 +86,10 @@ class LcnBinarySensor(LcnEntity, BinarySensorEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler( - self.bin_sensor_port - ) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler( + self.bin_sensor_port + ) @property def is_on(self): @@ -116,7 +118,8 @@ class LcnLockKeysSensor(LcnEntity, BinarySensorEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.source) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.source) @property def is_on(self): diff --git a/homeassistant/components/lcn/climate.py b/homeassistant/components/lcn/climate.py index e3eb92a426f..ece3994f651 100644 --- a/homeassistant/components/lcn/climate.py +++ b/homeassistant/components/lcn/climate.py @@ -63,8 +63,9 @@ class LcnClimate(LcnEntity, ClimateEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.variable) - await self.device_connection.activate_status_request_handler(self.setpoint) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.variable) + await self.device_connection.activate_status_request_handler(self.setpoint) @property def supported_features(self): diff --git a/homeassistant/components/lcn/cover.py b/homeassistant/components/lcn/cover.py index c5e407573ba..3d7c2a06a3b 100644 --- a/homeassistant/components/lcn/cover.py +++ b/homeassistant/components/lcn/cover.py @@ -161,7 +161,8 @@ class LcnRelayCover(LcnEntity, CoverEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.motor) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.motor) @property def is_closed(self): diff --git a/homeassistant/components/lcn/light.py b/homeassistant/components/lcn/light.py index c6ef895b7df..5242ed1cc59 100644 --- a/homeassistant/components/lcn/light.py +++ b/homeassistant/components/lcn/light.py @@ -68,7 +68,8 @@ class LcnOutputLight(LcnEntity, LightEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.output) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.output) @property def supported_features(self): @@ -155,7 +156,8 @@ class LcnRelayLight(LcnEntity, LightEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.output) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.output) @property def is_on(self): diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index f07c4d9c646..c5077bdf409 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -2,6 +2,10 @@ "domain": "lcn", "name": "LCN", "documentation": "https://www.home-assistant.io/integrations/lcn", - "requirements": ["pypck==0.7.7"], - "codeowners": ["@alengwenus"] + "requirements": [ + "pypck==0.7.9" + ], + "codeowners": [ + "@alengwenus" + ] } diff --git a/homeassistant/components/lcn/sensor.py b/homeassistant/components/lcn/sensor.py index 26b54def974..4d4be5e1259 100644 --- a/homeassistant/components/lcn/sensor.py +++ b/homeassistant/components/lcn/sensor.py @@ -57,7 +57,8 @@ class LcnVariableSensor(LcnEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.variable) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.variable) @property def state(self): @@ -98,7 +99,8 @@ class LcnLedLogicSensor(LcnEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.source) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.source) @property def state(self): diff --git a/homeassistant/components/lcn/switch.py b/homeassistant/components/lcn/switch.py index 5891629627e..6f9cc25db99 100644 --- a/homeassistant/components/lcn/switch.py +++ b/homeassistant/components/lcn/switch.py @@ -50,7 +50,8 @@ class LcnOutputSwitch(LcnEntity, SwitchEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.output) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.output) @property def is_on(self): @@ -97,7 +98,8 @@ class LcnRelaySwitch(LcnEntity, SwitchEntity): async def async_added_to_hass(self): """Run when entity about to be added to hass.""" await super().async_added_to_hass() - await self.device_connection.activate_status_request_handler(self.output) + if not self.device_connection.is_group: + await self.device_connection.activate_status_request_handler(self.output) @property def is_on(self): diff --git a/homeassistant/components/lg_soundbar/media_player.py b/homeassistant/components/lg_soundbar/media_player.py index 9bc0ba08e25..ee396a7a9ee 100644 --- a/homeassistant/components/lg_soundbar/media_player.py +++ b/homeassistant/components/lg_soundbar/media_player.py @@ -121,11 +121,6 @@ class LGDevice(MediaPlayerEntity): self._device.get_settings() self._device.get_product_info() - # Temporary fix until handling of unknown equaliser settings is integrated in the temescal library - for equaliser in self._equalisers: - if equaliser >= len(temescal.equalisers): - temescal.equalisers.append("unknown " + str(equaliser)) - @property def unique_id(self): """Return the device's unique ID.""" @@ -171,7 +166,7 @@ class LGDevice(MediaPlayerEntity): @property def source(self): """Return the current input source.""" - if self._function == -1: + if self._function == -1 or self._function >= len(temescal.functions): return None return temescal.functions[self._function] diff --git a/homeassistant/components/life360/translations/de.json b/homeassistant/components/life360/translations/de.json index 731ebdceef7..7e495987b45 100644 --- a/homeassistant/components/life360/translations/de.json +++ b/homeassistant/components/life360/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "create_entry": { @@ -8,6 +9,7 @@ }, "error": { "already_configured": "Konto ist bereits konfiguriert", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "invalid_username": "Ung\u00fcltiger Benutzername", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/life360/translations/fr.json b/homeassistant/components/life360/translations/fr.json index 72f56ed8784..cb86d8c6590 100644 --- a/homeassistant/components/life360/translations/fr.json +++ b/homeassistant/components/life360/translations/fr.json @@ -8,7 +8,7 @@ "default": "Pour d\u00e9finir les options avanc\u00e9es, voir [Documentation de Life360]( {docs_url} )." }, "error": { - "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9", + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", "invalid_auth": "Authentification invalide", "invalid_username": "Nom d'utilisateur invalide", "unknown": "Erreur inattendue" diff --git a/homeassistant/components/life360/translations/tr.json b/homeassistant/components/life360/translations/tr.json index 3f923c096cd..e1e57b39737 100644 --- a/homeassistant/components/life360/translations/tr.json +++ b/homeassistant/components/life360/translations/tr.json @@ -1,11 +1,22 @@ { "config": { "abort": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "unknown": "Beklenmedik hata" }, "error": { "already_configured": "Hesap zaten konfig\u00fcre edilmi\u015fi durumda", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_username": "Ge\u00e7ersiz kullan\u0131c\u0131 ad\u0131", "unknown": "Beklenmedik hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/life360/translations/uk.json b/homeassistant/components/life360/translations/uk.json new file mode 100644 index 00000000000..caecf494388 --- /dev/null +++ b/homeassistant/components/life360/translations/uk.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "create_entry": { + "default": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u043e\u0437\u0448\u0438\u0440\u0435\u043d\u0438\u0445 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u044c." + }, + "error": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "invalid_username": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0435 \u0456\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u043e\u0437\u0448\u0438\u0440\u0435\u043d\u0438\u0445 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u044c. \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0437\u0440\u043e\u0431\u0438\u0442\u0438 \u0446\u0435 \u043f\u0435\u0440\u0435\u0434 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.", + "title": "Life360" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/manifest.json b/homeassistant/components/lifx/manifest.json index 3f5ce03e672..d76f18c695f 100644 --- a/homeassistant/components/lifx/manifest.json +++ b/homeassistant/components/lifx/manifest.json @@ -3,7 +3,7 @@ "name": "LIFX", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/lifx", - "requirements": ["aiolifx==0.6.7", "aiolifx_effects==0.2.2"], + "requirements": ["aiolifx==0.6.9", "aiolifx_effects==0.2.2"], "homekit": { "models": ["LIFX"] }, diff --git a/homeassistant/components/lifx/translations/de.json b/homeassistant/components/lifx/translations/de.json index f88e27ff168..83eded1ddc6 100644 --- a/homeassistant/components/lifx/translations/de.json +++ b/homeassistant/components/lifx/translations/de.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "no_devices_found": "Keine LIFX Ger\u00e4te im Netzwerk gefunden.", - "single_instance_allowed": "Nur eine einzige Konfiguration von LIFX ist zul\u00e4ssig." + "no_devices_found": "Keine LIFX Ger\u00e4te im Netzwerk gefunden", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "step": { "confirm": { diff --git a/homeassistant/components/lifx/translations/tr.json b/homeassistant/components/lifx/translations/tr.json new file mode 100644 index 00000000000..fc7532a1e34 --- /dev/null +++ b/homeassistant/components/lifx/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "confirm": { + "description": "LIFX'i kurmak istiyor musunuz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lifx/translations/uk.json b/homeassistant/components/lifx/translations/uk.json new file mode 100644 index 00000000000..8c32e79533d --- /dev/null +++ b/homeassistant/components/lifx/translations/uk.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "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." + }, + "step": { + "confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 LIFX?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index f406366dc86..c46b7568b59 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -1,8 +1,10 @@ """Provides functionality to interact with lights.""" import csv +import dataclasses from datetime import timedelta import logging import os +from typing import Dict, List, Optional, Tuple, cast import voluptuous as vol @@ -21,6 +23,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 ) from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.entity_component import EntityComponent +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import bind_hass import homeassistant.util.color as color_util @@ -204,9 +207,6 @@ async def async_setup(hass, config): """ params = call.data["params"] - if not params: - profiles.apply_default(light.entity_id, params) - # Only process params once we processed brightness step if params and ( ATTR_BRIGHTNESS_STEP in params or ATTR_BRIGHTNESS_STEP_PCT in params @@ -223,6 +223,9 @@ async def async_setup(hass, config): preprocess_turn_on_alternatives(hass, params) + if ATTR_PROFILE not in params: + profiles.apply_default(light.entity_id, params) + # Zero brightness: Light will be turned off if params.get(ATTR_BRIGHTNESS) == 0: await light.async_turn_off(**filter_turn_off_params(params)) @@ -270,24 +273,74 @@ async def async_unload_entry(hass, entry): return await hass.data[DOMAIN].async_unload_entry(entry) -class Profiles: - """Representation of available color profiles.""" +def _coerce_none(value: str) -> None: + """Coerce an empty string as None.""" - SCHEMA = vol.Schema( + if not isinstance(value, str): + raise vol.Invalid("Expected a string") + + if value: + raise vol.Invalid("Not an empty string") + + +@dataclasses.dataclass +class Profile: + """Representation of a profile.""" + + name: str + color_x: Optional[float] = dataclasses.field(repr=False) + color_y: Optional[float] = dataclasses.field(repr=False) + brightness: Optional[int] + transition: Optional[int] = None + hs_color: Optional[Tuple[float, float]] = dataclasses.field(init=False) + + SCHEMA = vol.Schema( # pylint: disable=invalid-name vol.Any( - vol.ExactSequence((str, cv.small_float, cv.small_float, cv.byte)), vol.ExactSequence( - (str, cv.small_float, cv.small_float, cv.byte, cv.positive_int) + ( + str, + vol.Any(cv.small_float, _coerce_none), + vol.Any(cv.small_float, _coerce_none), + vol.Any(cv.byte, _coerce_none), + ) + ), + vol.ExactSequence( + ( + str, + vol.Any(cv.small_float, _coerce_none), + vol.Any(cv.small_float, _coerce_none), + vol.Any(cv.byte, _coerce_none), + vol.Any(VALID_TRANSITION, _coerce_none), + ) ), ) ) - def __init__(self, hass): + def __post_init__(self) -> None: + """Convert xy to hs color.""" + if None in (self.color_x, self.color_y): + self.hs_color = None + return + + self.hs_color = color_util.color_xy_to_hs( + cast(float, self.color_x), cast(float, self.color_y) + ) + + @classmethod + def from_csv_row(cls, csv_row: List[str]) -> "Profile": + """Create profile from a CSV row tuple.""" + return cls(*cls.SCHEMA(csv_row)) + + +class Profiles: + """Representation of available color profiles.""" + + def __init__(self, hass: HomeAssistantType): """Initialize profiles.""" self.hass = hass - self.data = None + self.data: Dict[str, Profile] = {} - def _load_profile_data(self): + def _load_profile_data(self) -> Dict[str, Profile]: """Load built-in profiles and custom profiles.""" profile_paths = [ os.path.join(os.path.dirname(__file__), LIGHT_PROFILES_FILE), @@ -306,56 +359,46 @@ class Profiles: try: for rec in reader: - ( - profile, - color_x, - color_y, - brightness, - *transition, - ) = Profiles.SCHEMA(rec) + profile = Profile.from_csv_row(rec) + profiles[profile.name] = profile - transition = transition[0] if transition else 0 - - profiles[profile] = color_util.color_xy_to_hs( - color_x, color_y - ) + ( - brightness, - transition, - ) except vol.MultipleInvalid as ex: _LOGGER.error( - "Error parsing light profile from %s: %s", profile_path, ex + "Error parsing light profile row '%s' from %s: %s", + rec, + profile_path, + ex, ) continue return profiles - async def async_initialize(self): + async def async_initialize(self) -> None: """Load and cache profiles.""" self.data = await self.hass.async_add_executor_job(self._load_profile_data) @callback - def apply_default(self, entity_id, params): + def apply_default(self, entity_id: str, params: Dict) -> None: """Return the default turn-on profile for the given light.""" - name = f"{entity_id}.default" - if name in self.data: - self.apply_profile(name, params) - return - - name = "group.all_lights.default" - if name in self.data: - self.apply_profile(name, params) + for _entity_id in (entity_id, "group.all_lights"): + name = f"{_entity_id}.default" + if name in self.data: + self.apply_profile(name, params) + return @callback - def apply_profile(self, name, params): + def apply_profile(self, name: str, params: Dict) -> None: """Apply a profile.""" profile = self.data.get(name) if profile is None: return - params.setdefault(ATTR_HS_COLOR, profile[:2]) - params.setdefault(ATTR_BRIGHTNESS, profile[2]) - params.setdefault(ATTR_TRANSITION, profile[3]) + if profile.hs_color is not None: + params.setdefault(ATTR_HS_COLOR, profile.hs_color) + if profile.brightness is not None: + params.setdefault(ATTR_BRIGHTNESS, profile.brightness) + if profile.transition is not None: + params.setdefault(ATTR_TRANSITION, profile.transition) class LightEntity(ToggleEntity): diff --git a/homeassistant/components/light/significant_change.py b/homeassistant/components/light/significant_change.py new file mode 100644 index 00000000000..a0bd5203101 --- /dev/null +++ b/homeassistant/components/light/significant_change.py @@ -0,0 +1,71 @@ +"""Helper to test significant Light state changes.""" +from typing import Any, Optional + +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.significant_change import ( + check_numeric_changed, + either_one_none, +) + +from . import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_HS_COLOR, + ATTR_WHITE_VALUE, +) + + +@callback +def async_check_significant_change( + hass: HomeAssistant, + old_state: str, + old_attrs: dict, + new_state: str, + new_attrs: dict, + **kwargs: Any, +) -> Optional[bool]: + """Test if state significantly changed.""" + if old_state != new_state: + return True + + if old_attrs.get(ATTR_EFFECT) != new_attrs.get(ATTR_EFFECT): + return True + + old_color = old_attrs.get(ATTR_HS_COLOR) + new_color = new_attrs.get(ATTR_HS_COLOR) + + if either_one_none(old_color, new_color): + return True + + if old_color and new_color: + # Range 0..360 + if check_numeric_changed(old_color[0], new_color[0], 5): + return True + + # Range 0..100 + if check_numeric_changed(old_color[1], new_color[1], 3): + return True + + if check_numeric_changed( + old_attrs.get(ATTR_BRIGHTNESS), new_attrs.get(ATTR_BRIGHTNESS), 3 + ): + return True + + if check_numeric_changed( + # Default range 153..500 + old_attrs.get(ATTR_COLOR_TEMP), + new_attrs.get(ATTR_COLOR_TEMP), + 5, + ): + return True + + if check_numeric_changed( + # Range 0..255 + old_attrs.get(ATTR_WHITE_VALUE), + new_attrs.get(ATTR_WHITE_VALUE), + 5, + ): + return True + + return False diff --git a/homeassistant/components/light/translations/uk.json b/homeassistant/components/light/translations/uk.json index 67685889c54..86eee7d6b23 100644 --- a/homeassistant/components/light/translations/uk.json +++ b/homeassistant/components/light/translations/uk.json @@ -1,5 +1,17 @@ { "device_automation": { + "action_type": { + "brightness_decrease": "{entity_name}: \u0437\u043c\u0435\u043d\u0448\u0438\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c", + "brightness_increase": "{entity_name}: \u0437\u0431\u0456\u043b\u044c\u0448\u0438\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c", + "flash": "{entity_name}: \u0432\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u043c\u0438\u0433\u0430\u043d\u043d\u044f", + "toggle": "{entity_name}: \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u0438", + "turn_off": "{entity_name}: \u0432\u0438\u043c\u043a\u043d\u0443\u0442\u0438", + "turn_on": "{entity_name}: \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438" + }, + "condition_type": { + "is_off": "{entity_name} \u0443 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_on": "{entity_name} \u0443 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456" + }, "trigger_type": { "turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e", "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e" diff --git a/homeassistant/components/lirc/__init__.py b/homeassistant/components/lirc/__init__.py index bfc8e455624..bf939fb7535 100644 --- a/homeassistant/components/lirc/__init__.py +++ b/homeassistant/components/lirc/__init__.py @@ -5,7 +5,6 @@ import threading import time import lirc -import voluptuous as vol from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP @@ -19,8 +18,6 @@ EVENT_IR_COMMAND_RECEIVED = "ir_command_received" ICON = "mdi:remote" -CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) - def setup(hass, config): """Set up the LIRC capability.""" diff --git a/homeassistant/components/local_ip/translations/de.json b/homeassistant/components/local_ip/translations/de.json index 072f6ec964d..9e2a6eda5c6 100644 --- a/homeassistant/components/local_ip/translations/de.json +++ b/homeassistant/components/local_ip/translations/de.json @@ -1,13 +1,14 @@ { "config": { "abort": { - "single_instance_allowed": "Es ist nur eine einzige Konfiguration der lokalen IP zul\u00e4ssig." + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "step": { "user": { "data": { "name": "Sensorname" }, + "description": "M\u00f6chtest du mit der Einrichtung beginnen?", "title": "Lokale IP-Adresse" } } diff --git a/homeassistant/components/local_ip/translations/es.json b/homeassistant/components/local_ip/translations/es.json index fe9a0ad1414..a3048d396d5 100644 --- a/homeassistant/components/local_ip/translations/es.json +++ b/homeassistant/components/local_ip/translations/es.json @@ -8,7 +8,7 @@ "data": { "name": "Nombre del sensor" }, - "description": "\u00bfQuieres empezar a configurar?", + "description": "\u00bfQuieres iniciar la configuraci\u00f3n?", "title": "Direcci\u00f3n IP local" } } diff --git a/homeassistant/components/local_ip/translations/tr.json b/homeassistant/components/local_ip/translations/tr.json new file mode 100644 index 00000000000..e8e82814f8a --- /dev/null +++ b/homeassistant/components/local_ip/translations/tr.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "user": { + "data": { + "name": "Sens\u00f6r Ad\u0131" + }, + "description": "Kuruluma ba\u015flamak ister misiniz?", + "title": "Yerel IP Adresi" + } + } + }, + "title": "Yerel IP Adresi" +} \ No newline at end of file diff --git a/homeassistant/components/local_ip/translations/uk.json b/homeassistant/components/local_ip/translations/uk.json new file mode 100644 index 00000000000..b88c1c002bf --- /dev/null +++ b/homeassistant/components/local_ip/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "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." + }, + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?", + "title": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u0430 IP-\u0430\u0434\u0440\u0435\u0441\u0430" + } + } + }, + "title": "\u041b\u043e\u043a\u0430\u043b\u044c\u043d\u0430 IP-\u0430\u0434\u0440\u0435\u0441\u0430" +} \ No newline at end of file diff --git a/homeassistant/components/locative/translations/de.json b/homeassistant/components/locative/translations/de.json index 32617094146..a6dcf4150d0 100644 --- a/homeassistant/components/locative/translations/de.json +++ b/homeassistant/components/locative/translations/de.json @@ -1,11 +1,15 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", + "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." + }, "create_entry": { "default": "Um Standorte Home Assistant zu senden, muss das Webhook Feature in der Locative App konfiguriert werden.\n\n F\u00fcge die folgenden Informationen ein: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n \n Weitere Informationen finden sich in der [Dokumentation]({docs_url})." }, "step": { "user": { - "description": "M\u00f6chtest du den Locative Webhook wirklich einrichten?", + "description": "M\u00f6chtest du mit der Einrichtung beginnen?", "title": "Locative Webhook einrichten" } } diff --git a/homeassistant/components/locative/translations/tr.json b/homeassistant/components/locative/translations/tr.json new file mode 100644 index 00000000000..84adcdf8225 --- /dev/null +++ b/homeassistant/components/locative/translations/tr.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/locative/translations/uk.json b/homeassistant/components/locative/translations/uk.json new file mode 100644 index 00000000000..d9a47130871 --- /dev/null +++ b/homeassistant/components/locative/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "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.", + "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c." + }, + "create_entry": { + "default": "\u0414\u043b\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043a\u0438 \u043f\u043e\u0434\u0456\u0439 \u0432 Home Assistant \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Webhook \u0434\u043b\u044f Locative. \n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e: \n\n- URL: `{webhook_url}`\n- Method: POST \n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457." + }, + "step": { + "user": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?", + "title": "Locative" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lock/translations/pt.json b/homeassistant/components/lock/translations/pt.json index 5ba9f10db14..44f30900572 100644 --- a/homeassistant/components/lock/translations/pt.json +++ b/homeassistant/components/lock/translations/pt.json @@ -5,6 +5,9 @@ "open": "Abrir {entity_name}", "unlock": "Desbloquear {entity_name}" }, + "condition_type": { + "is_unlocked": "{entity_name} est\u00e1 destrancado" + }, "trigger_type": { "locked": "{entity_name} fechada", "unlocked": "{entity_name} aberta" diff --git a/homeassistant/components/lock/translations/tr.json b/homeassistant/components/lock/translations/tr.json index 95b50398fda..ea6ff1a157d 100644 --- a/homeassistant/components/lock/translations/tr.json +++ b/homeassistant/components/lock/translations/tr.json @@ -1,4 +1,10 @@ { + "device_automation": { + "trigger_type": { + "locked": "{entity_name} kilitlendi", + "unlocked": "{entity_name} kilidi a\u00e7\u0131ld\u0131" + } + }, "state": { "_": { "locked": "Kilitli", diff --git a/homeassistant/components/lock/translations/uk.json b/homeassistant/components/lock/translations/uk.json index d919252eb56..96b92012e9d 100644 --- a/homeassistant/components/lock/translations/uk.json +++ b/homeassistant/components/lock/translations/uk.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "lock": "{entity_name}: \u0437\u0430\u0431\u043b\u043e\u043a\u0443\u0432\u0430\u0442\u0438", + "open": "{entity_name}: \u0432\u0456\u0434\u043a\u0440\u0438\u0442\u0438", + "unlock": "{entity_name}: \u0440\u043e\u0437\u0431\u043b\u043e\u043a\u0443\u0432\u0430\u0442\u0438" + }, + "condition_type": { + "is_locked": "{entity_name} \u0432 \u0437\u0430\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_unlocked": "{entity_name} \u0432 \u0440\u043e\u0437\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456" + }, + "trigger_type": { + "locked": "{entity_name} \u0431\u043b\u043e\u043a\u0443\u0454\u0442\u044c\u0441\u044f", + "unlocked": "{entity_name} \u0440\u043e\u0437\u0431\u043b\u043e\u043a\u0443\u0454\u0442\u044c\u0441\u044f" + } + }, "state": { "_": { "locked": "\u0417\u0430\u0431\u043b\u043e\u043a\u043e\u0432\u0430\u043d\u043e", diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 254d99ed848..e2d8a22c251 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -93,6 +93,7 @@ EVENT_COLUMNS = [ Events.time_fired, Events.context_id, Events.context_user_id, + Events.context_parent_id, ] SCRIPT_AUTOMATION_EVENTS = [EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED] @@ -320,16 +321,14 @@ def humanify(hass, events, entity_attr_cache, context_lookup): if event.context_user_id: data["context_user_id"] = event.context_user_id - context_event = context_lookup.get(event.context_id) - if context_event and context_event != event: - _augment_data_with_context( - data, - entity_id, - event, - context_event, - entity_attr_cache, - external_events, - ) + _augment_data_with_context( + data, + entity_id, + event, + context_lookup, + entity_attr_cache, + external_events, + ) yield data @@ -340,16 +339,15 @@ def humanify(hass, events, entity_attr_cache, context_lookup): data["domain"] = domain if event.context_user_id: data["context_user_id"] = event.context_user_id - context_event = context_lookup.get(event.context_id) - if context_event: - _augment_data_with_context( - data, - data.get(ATTR_ENTITY_ID), - event, - context_event, - entity_attr_cache, - external_events, - ) + + _augment_data_with_context( + data, + data.get(ATTR_ENTITY_ID), + event, + context_lookup, + entity_attr_cache, + external_events, + ) yield data elif event.event_type == EVENT_HOMEASSISTANT_START: @@ -397,16 +395,14 @@ def humanify(hass, events, entity_attr_cache, context_lookup): if event.context_user_id: data["context_user_id"] = event.context_user_id - context_event = context_lookup.get(event.context_id) - if context_event and context_event != event: - _augment_data_with_context( - data, - entity_id, - event, - context_event, - entity_attr_cache, - external_events, - ) + _augment_data_with_context( + data, + entity_id, + event, + context_lookup, + entity_attr_cache, + external_events, + ) yield data @@ -597,16 +593,27 @@ def _keep_event(hass, event, entities_filter): def _augment_data_with_context( - data, entity_id, event, context_event, entity_attr_cache, external_events + data, entity_id, event, context_lookup, entity_attr_cache, external_events ): - event_type = context_event.event_type + context_event = context_lookup.get(event.context_id) - # State change - context_entity_id = context_event.entity_id - - if entity_id and context_entity_id == entity_id: + if not context_event: return + if event == context_event: + # This is the first event with the given ID. Was it directly caused by + # a parent event? + if event.context_parent_id: + context_event = context_lookup.get(event.context_parent_id) + # Ensure the (parent) context_event exists and is not the root cause of + # this log entry. + if not context_event or event == context_event: + return + + event_type = context_event.event_type + context_entity_id = context_event.entity_id + + # State change if context_entity_id: data["context_entity_id"] = context_entity_id data["context_entity_id_name"] = _entity_name_from_event( @@ -672,6 +679,7 @@ class LazyEventPartialState: "domain", "context_id", "context_user_id", + "context_parent_id", "time_fired_minute", ] @@ -687,6 +695,7 @@ class LazyEventPartialState: self.domain = self._row.domain self.context_id = self._row.context_id self.context_user_id = self._row.context_user_id + self.context_parent_id = self._row.context_parent_id self.time_fired_minute = self._row.time_fired.minute @property diff --git a/homeassistant/components/logi_circle/translations/de.json b/homeassistant/components/logi_circle/translations/de.json index ab4a194fda0..1eec1d3c4a5 100644 --- a/homeassistant/components/logi_circle/translations/de.json +++ b/homeassistant/components/logi_circle/translations/de.json @@ -1,11 +1,15 @@ { "config": { "abort": { + "already_configured": "Konto wurde bereits konfiguriert", "external_error": "Es ist eine Ausnahme in einem anderen Flow aufgetreten.", - "external_setup": "Logi Circle wurde erfolgreich aus einem anderen Flow konfiguriert." + "external_setup": "Logi Circle wurde erfolgreich aus einem anderen Flow konfiguriert.", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen." }, "error": { - "follow_link": "Bitte folge dem Link und authentifiziere dich, bevor du auf Senden klickst." + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "follow_link": "Bitte folge dem Link und authentifiziere dich, bevor du auf Senden klickst.", + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { "auth": { diff --git a/homeassistant/components/logi_circle/translations/fr.json b/homeassistant/components/logi_circle/translations/fr.json index 7ac388ccb3f..6bd22f473e7 100644 --- a/homeassistant/components/logi_circle/translations/fr.json +++ b/homeassistant/components/logi_circle/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9", + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", "external_error": "Une exception est survenue \u00e0 partir d'un autre flux.", "external_setup": "Logi Circle a \u00e9t\u00e9 configur\u00e9 avec succ\u00e8s \u00e0 partir d'un autre flux.", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation." diff --git a/homeassistant/components/logi_circle/translations/lb.json b/homeassistant/components/logi_circle/translations/lb.json index fab157b2655..82be2f6a82d 100644 --- a/homeassistant/components/logi_circle/translations/lb.json +++ b/homeassistant/components/logi_circle/translations/lb.json @@ -7,6 +7,7 @@ "missing_configuration": "Komponent net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun." }, "error": { + "authorize_url_timeout": "Z\u00e4itiwwerschreidung beim erstellen vun der Authorisatiouns URL.", "follow_link": "Follegt w.e.g dem Link an authentifiz\u00e9iert iech ier de op Ofsch\u00e9cken dr\u00e9ckt.", "invalid_auth": "Ong\u00eblteg Authentifikatioun" }, diff --git a/homeassistant/components/logi_circle/translations/tr.json b/homeassistant/components/logi_circle/translations/tr.json new file mode 100644 index 00000000000..0b0f58116c2 --- /dev/null +++ b/homeassistant/components/logi_circle/translations/tr.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/logi_circle/translations/uk.json b/homeassistant/components/logi_circle/translations/uk.json new file mode 100644 index 00000000000..2c021992413 --- /dev/null +++ b/homeassistant/components/logi_circle/translations/uk.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "external_error": "\u0412\u0438\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0432\u0456\u0434\u0431\u0443\u043b\u043e\u0441\u044f \u0437 \u0456\u043d\u0448\u043e\u0433\u043e \u043f\u043e\u0442\u043e\u043a\u0443.", + "external_setup": "Logi Circle \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439 \u0437 \u0456\u043d\u0448\u043e\u0433\u043e \u043f\u043e\u0442\u043e\u043a\u0443.", + "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438." + }, + "error": { + "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "follow_link": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0437\u0430 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f\u043c \u0456 \u043f\u0440\u043e\u0439\u0434\u0456\u0442\u044c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e, \u043f\u0435\u0440\u0448 \u043d\u0456\u0436 \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0438 \"\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0438\".", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "step": { + "auth": { + "description": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u043f\u043e [\u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f\u043c]({authorization_url}) \u0456 ** \u0414\u043e\u0437\u0432\u043e\u043b\u044c\u0442\u0435 ** \u0434\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u0432\u0430\u0448\u043e\u0433\u043e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 Logi Circle, \u043f\u043e\u0442\u0456\u043c \u043f\u043e\u0432\u0435\u0440\u043d\u0456\u0442\u044c\u0441\u044f \u0441\u044e\u0434\u0438 \u0456 \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c ** \u041f\u0406\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u0418 **.", + "title": "Logi Circle" + }, + "user": { + "data": { + "flow_impl": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457, \u0447\u0435\u0440\u0435\u0437 \u044f\u043a\u0438\u0439 \u0431\u0443\u0434\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0438\u0439 \u0432\u0445\u0456\u0434.", + "title": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 7d0fe6574b9..99b00a92289 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -12,7 +12,6 @@ from homeassistant.helpers import collection, config_validation as cv from homeassistant.helpers.service import async_register_admin_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceCallType from homeassistant.loader import async_get_integration -from homeassistant.util import sanitize_path from . import dashboard, resources, websocket from .const import ( @@ -47,7 +46,7 @@ YAML_DASHBOARD_SCHEMA = vol.Schema( { **DASHBOARD_BASE_CREATE_FIELDS, vol.Required(CONF_MODE): MODE_YAML, - vol.Required(CONF_FILENAME): vol.All(cv.string, sanitize_path), + vol.Required(CONF_FILENAME): cv.path, } ) diff --git a/homeassistant/components/lovelace/const.py b/homeassistant/components/lovelace/const.py index a093c672dd6..e93649de451 100644 --- a/homeassistant/components/lovelace/const.py +++ b/homeassistant/components/lovelace/const.py @@ -16,6 +16,7 @@ DEFAULT_ICON = "hass:view-dashboard" CONF_MODE = "mode" MODE_YAML = "yaml" MODE_STORAGE = "storage" +MODE_AUTO = "auto-gen" LOVELACE_CONFIG_FILE = "ui-lovelace.yaml" CONF_RESOURCES = "resources" diff --git a/homeassistant/components/lovelace/system_health.py b/homeassistant/components/lovelace/system_health.py index e0d1152a049..a148427c9bd 100644 --- a/homeassistant/components/lovelace/system_health.py +++ b/homeassistant/components/lovelace/system_health.py @@ -1,8 +1,10 @@ """Provide info to system health.""" +import asyncio + from homeassistant.components import system_health from homeassistant.core import HomeAssistant, callback -from .const import DOMAIN +from .const import CONF_MODE, DOMAIN, MODE_AUTO, MODE_STORAGE, MODE_YAML @callback @@ -16,6 +18,30 @@ def async_register( async def system_health_info(hass): """Get info for the info page.""" health_info = {"dashboards": len(hass.data[DOMAIN]["dashboards"])} - health_info.update(await hass.data[DOMAIN]["dashboards"][None].async_get_info()) health_info.update(await hass.data[DOMAIN]["resources"].async_get_info()) + + dashboards_info = await asyncio.gather( + *[ + hass.data[DOMAIN]["dashboards"][dashboard].async_get_info() + for dashboard in hass.data[DOMAIN]["dashboards"] + ] + ) + + modes = set() + for dashboard in dashboards_info: + for key in dashboard: + if isinstance(dashboard[key], int): + health_info[key] = health_info.get(key, 0) + dashboard[key] + elif key == CONF_MODE: + modes.add(dashboard[key]) + else: + health_info[key] = dashboard[key] + + if MODE_STORAGE in modes: + health_info[CONF_MODE] = MODE_STORAGE + elif MODE_YAML in modes: + health_info[CONF_MODE] = MODE_YAML + else: + health_info[CONF_MODE] = MODE_AUTO + return health_info diff --git a/homeassistant/components/lovelace/translations/de.json b/homeassistant/components/lovelace/translations/de.json index c8680fcb7e5..b6c7562f0ec 100644 --- a/homeassistant/components/lovelace/translations/de.json +++ b/homeassistant/components/lovelace/translations/de.json @@ -1,6 +1,9 @@ { "system_health": { "info": { + "dashboards": "Dashboards", + "mode": "Modus", + "resources": "Ressourcen", "views": "Ansichten" } } diff --git a/homeassistant/components/lovelace/translations/fr.json b/homeassistant/components/lovelace/translations/fr.json new file mode 100644 index 00000000000..f2847bcc177 --- /dev/null +++ b/homeassistant/components/lovelace/translations/fr.json @@ -0,0 +1,10 @@ +{ + "system_health": { + "info": { + "dashboards": "Tableaux de bord", + "mode": "Mode", + "resources": "Ressources", + "views": "Vues" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/tr.json b/homeassistant/components/lovelace/translations/tr.json index 9f763d0d6cc..d159e058ffa 100644 --- a/homeassistant/components/lovelace/translations/tr.json +++ b/homeassistant/components/lovelace/translations/tr.json @@ -3,7 +3,8 @@ "info": { "dashboards": "Kontrol panelleri", "mode": "Mod", - "resources": "Kaynaklar" + "resources": "Kaynaklar", + "views": "G\u00f6r\u00fcn\u00fcmler" } } } \ No newline at end of file diff --git a/homeassistant/components/lovelace/translations/uk.json b/homeassistant/components/lovelace/translations/uk.json new file mode 100644 index 00000000000..21d97fd14c3 --- /dev/null +++ b/homeassistant/components/lovelace/translations/uk.json @@ -0,0 +1,10 @@ +{ + "system_health": { + "info": { + "dashboards": "\u041f\u0430\u043d\u0435\u043b\u0456", + "mode": "\u0420\u0435\u0436\u0438\u043c", + "resources": "\u0420\u0435\u0441\u0443\u0440\u0441\u0438", + "views": "\u0412\u043a\u043b\u0430\u0434\u043a\u0438" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/luftdaten/translations/de.json b/homeassistant/components/luftdaten/translations/de.json index 122dc611870..499a65623b0 100644 --- a/homeassistant/components/luftdaten/translations/de.json +++ b/homeassistant/components/luftdaten/translations/de.json @@ -1,6 +1,7 @@ { "config": { "error": { + "already_configured": "Der Dienst ist bereits konfiguriert", "cannot_connect": "Verbindung fehlgeschlagen", "invalid_sensor": "Sensor nicht verf\u00fcgbar oder ung\u00fcltig" }, diff --git a/homeassistant/components/luftdaten/translations/tr.json b/homeassistant/components/luftdaten/translations/tr.json new file mode 100644 index 00000000000..04565de3d28 --- /dev/null +++ b/homeassistant/components/luftdaten/translations/tr.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/luftdaten/translations/uk.json b/homeassistant/components/luftdaten/translations/uk.json new file mode 100644 index 00000000000..9fd33dc3da2 --- /dev/null +++ b/homeassistant/components/luftdaten/translations/uk.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_sensor": "\u0421\u0435\u043d\u0441\u043e\u0440 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0430\u0431\u043e \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439." + }, + "step": { + "user": { + "data": { + "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438 \u043d\u0430 \u043c\u0430\u043f\u0456", + "station_id": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0441\u0435\u043d\u0441\u043e\u0440\u0430 Luftdaten" + }, + "title": "Luftdaten" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron/cover.py b/homeassistant/components/lutron/cover.py index 1ec7c07aac0..f1faed32161 100644 --- a/homeassistant/components/lutron/cover.py +++ b/homeassistant/components/lutron/cover.py @@ -66,4 +66,4 @@ class LutronCover(LutronDevice, CoverEntity): @property def device_state_attributes(self): """Return the state attributes.""" - return {"Lutron Integration ID": self._lutron_device.id} + return {"lutron_integration_id": self._lutron_device.id} diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index 612c6a26d1b..73eb0b83fa6 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -1,19 +1,48 @@ """Component for interacting with a Lutron Caseta system.""" +import asyncio import logging +import ssl +from aiolip import LIP +from aiolip.data import LIPMode +from aiolip.protocol import LIP_BUTTON_PRESS +import async_timeout from pylutron_caseta.smartbridge import Smartbridge import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_HOST +from homeassistant.core import callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from .const import CONF_CA_CERTS, CONF_CERTFILE, CONF_KEYFILE +from .const import ( + ACTION_PRESS, + ACTION_RELEASE, + ATTR_ACTION, + ATTR_AREA_NAME, + ATTR_BUTTON_NUMBER, + ATTR_DEVICE_NAME, + ATTR_SERIAL, + ATTR_TYPE, + BRIDGE_DEVICE, + BRIDGE_DEVICE_ID, + BRIDGE_LEAP, + BRIDGE_LIP, + BRIDGE_TIMEOUT, + BUTTON_DEVICES, + CONF_CA_CERTS, + CONF_CERTFILE, + CONF_KEYFILE, + DOMAIN, + LUTRON_CASETA_BUTTON_EVENT, + MANUFACTURER, +) _LOGGER = logging.getLogger(__name__) -DOMAIN = "lutron_caseta" DATA_BRIDGE_CONFIG = "lutron_caseta_bridges" CONFIG_SCHEMA = vol.Schema( @@ -39,23 +68,24 @@ LUTRON_CASETA_COMPONENTS = ["light", "switch", "cover", "scene", "fan", "binary_ async def async_setup(hass, base_config): """Set up the Lutron component.""" - bridge_configs = base_config[DOMAIN] hass.data.setdefault(DOMAIN, {}) - for config in bridge_configs: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - # extract the config keys one-by-one just to be explicit - data={ - CONF_HOST: config[CONF_HOST], - CONF_KEYFILE: config[CONF_KEYFILE], - CONF_CERTFILE: config[CONF_CERTFILE], - CONF_CA_CERTS: config[CONF_CA_CERTS], - }, + if DOMAIN in base_config: + bridge_configs = base_config[DOMAIN] + for config in bridge_configs: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + # extract the config keys one-by-one just to be explicit + data={ + CONF_HOST: config[CONF_HOST], + CONF_KEYFILE: config[CONF_KEYFILE], + CONF_CERTFILE: config[CONF_CERTFILE], + CONF_CA_CERTS: config[CONF_CA_CERTS], + }, + ) ) - ) return True @@ -67,21 +97,47 @@ async def async_setup_entry(hass, config_entry): keyfile = hass.config.path(config_entry.data[CONF_KEYFILE]) certfile = hass.config.path(config_entry.data[CONF_CERTFILE]) ca_certs = hass.config.path(config_entry.data[CONF_CA_CERTS]) + bridge = None - bridge = Smartbridge.create_tls( - hostname=host, keyfile=keyfile, certfile=certfile, ca_certs=ca_certs - ) - - await bridge.connect() - if not bridge.is_connected(): - _LOGGER.error("Unable to connect to Lutron Caseta bridge at %s", host) + try: + bridge = Smartbridge.create_tls( + hostname=host, keyfile=keyfile, certfile=certfile, ca_certs=ca_certs + ) + except ssl.SSLError: + _LOGGER.error("Invalid certificate used to connect to bridge at %s", host) return False - _LOGGER.debug("Connected to Lutron Caseta bridge at %s", host) + timed_out = True + try: + async with async_timeout.timeout(BRIDGE_TIMEOUT): + await bridge.connect() + timed_out = False + except asyncio.TimeoutError: + _LOGGER.error("Timeout while trying to connect to bridge at %s", host) + if timed_out or not bridge.is_connected(): + await bridge.close() + raise ConfigEntryNotReady + + _LOGGER.debug("Connected to Lutron Caseta bridge via LEAP at %s", host) + + devices = bridge.get_devices() + bridge_device = devices[BRIDGE_DEVICE_ID] + await _async_register_bridge_device(hass, config_entry.entry_id, bridge_device) # Store this bridge (keyed by entry_id) so it can be retrieved by the # components we're setting up. - hass.data[DOMAIN][config_entry.entry_id] = bridge + hass.data[DOMAIN][config_entry.entry_id] = { + BRIDGE_LEAP: bridge, + BRIDGE_DEVICE: bridge_device, + BUTTON_DEVICES: {}, + BRIDGE_LIP: None, + } + + if bridge.lip_devices: + # If the bridge also supports LIP (Lutron Integration Protocol) + # we can fire events when pico buttons are pressed to allow + # pico remotes to control other devices. + await async_setup_lip(hass, config_entry, bridge.lip_devices) for component in LUTRON_CASETA_COMPONENTS: hass.async_create_task( @@ -91,17 +147,168 @@ async def async_setup_entry(hass, config_entry): return True +async def async_setup_lip(hass, config_entry, lip_devices): + """Connect to the bridge via Lutron Integration Protocol to watch for pico remotes.""" + host = config_entry.data[CONF_HOST] + config_entry_id = config_entry.entry_id + data = hass.data[DOMAIN][config_entry_id] + bridge_device = data[BRIDGE_DEVICE] + bridge = data[BRIDGE_LEAP] + lip = LIP() + try: + await lip.async_connect(host) + except asyncio.TimeoutError: + _LOGGER.error("Failed to connect to via LIP at %s:23", host) + return + + _LOGGER.debug("Connected to Lutron Caseta bridge via LIP at %s:23", host) + button_devices_by_lip_id = _async_merge_lip_leap_data(lip_devices, bridge) + button_devices_by_dr_id = await _async_register_button_devices( + hass, config_entry_id, bridge_device, button_devices_by_lip_id + ) + _async_subscribe_pico_remote_events(hass, lip, button_devices_by_lip_id) + data[BUTTON_DEVICES] = button_devices_by_dr_id + data[BRIDGE_LIP] = lip + + +@callback +def _async_merge_lip_leap_data(lip_devices, bridge): + """Merge the leap data into the lip data.""" + sensor_devices = bridge.get_devices_by_domain("sensor") + + button_devices_by_id = { + id: device for id, device in lip_devices.items() if "Buttons" in device + } + sensor_devices_by_name = {device["name"]: device for device in sensor_devices} + + # Add the leap data into the lip data + # so we know the type, model, and serial + for device in button_devices_by_id.values(): + area = device.get("Area", {}).get("Name", "") + name = device["Name"] + leap_name = f"{area}_{name}" + device["leap_name"] = leap_name + leap_device_data = sensor_devices_by_name.get(leap_name) + if leap_device_data is None: + continue + for key in ("type", "model", "serial"): + val = leap_device_data.get(key) + if val is not None: + device[key] = val + + _LOGGER.debug("Button Devices: %s", button_devices_by_id) + return button_devices_by_id + + +async def _async_register_bridge_device(hass, config_entry_id, bridge_device): + """Register the bridge device in the device registry.""" + device_registry = await dr.async_get_registry(hass) + device_registry.async_get_or_create( + name=bridge_device["name"], + manufacturer=MANUFACTURER, + config_entry_id=config_entry_id, + identifiers={(DOMAIN, bridge_device["serial"])}, + model=f"{bridge_device['model']} ({bridge_device['type']})", + ) + + +async def _async_register_button_devices( + hass, config_entry_id, bridge_device, button_devices_by_id +): + """Register button devices (Pico Remotes) in the device registry.""" + device_registry = await dr.async_get_registry(hass) + button_devices_by_dr_id = {} + + for device in button_devices_by_id.values(): + if "serial" not in device: + continue + + dr_device = device_registry.async_get_or_create( + name=device["leap_name"], + manufacturer=MANUFACTURER, + config_entry_id=config_entry_id, + identifiers={(DOMAIN, device["serial"])}, + model=f"{device['model']} ({device['type']})", + via_device=(DOMAIN, bridge_device["serial"]), + ) + + button_devices_by_dr_id[dr_device.id] = device + + return button_devices_by_dr_id + + +@callback +def _async_subscribe_pico_remote_events(hass, lip, button_devices_by_id): + """Subscribe to lutron events.""" + + @callback + def _async_lip_event(lip_message): + if lip_message.mode != LIPMode.DEVICE: + return + + device = button_devices_by_id.get(lip_message.integration_id) + + if not device: + return + + if lip_message.value == LIP_BUTTON_PRESS: + action = ACTION_PRESS + else: + action = ACTION_RELEASE + + hass.bus.async_fire( + LUTRON_CASETA_BUTTON_EVENT, + { + ATTR_SERIAL: device.get("serial"), + ATTR_TYPE: device.get("type"), + ATTR_BUTTON_NUMBER: lip_message.action_number, + ATTR_DEVICE_NAME: device["Name"], + ATTR_AREA_NAME: device.get("Area", {}).get("Name"), + ATTR_ACTION: action, + }, + ) + + lip.subscribe(_async_lip_event) + + asyncio.create_task(lip.async_run()) + + +async def async_unload_entry(hass, config_entry): + """Unload the bridge bridge from a config entry.""" + + data = hass.data[DOMAIN][config_entry.entry_id] + data[BRIDGE_LEAP].close() + if data[BRIDGE_LIP]: + await data[BRIDGE_LIP].async_stop() + + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in LUTRON_CASETA_COMPONENTS + ] + ) + ) + + if unload_ok: + hass.data[DOMAIN].pop(config_entry.entry_id) + + return unload_ok + + class LutronCasetaDevice(Entity): """Common base class for all Lutron Caseta devices.""" - def __init__(self, device, bridge): + def __init__(self, device, bridge, bridge_device): """Set up the base class. [:param]device the device metadata [:param]bridge the smartbridge object + [:param]bridge_device a dict with the details of the bridge """ self._device = device self._smartbridge = bridge + self._bridge_device = bridge_device async def async_added_to_hass(self): """Register callbacks.""" @@ -133,8 +340,9 @@ class LutronCasetaDevice(Entity): return { "identifiers": {(DOMAIN, self.serial)}, "name": self.name, - "manufacturer": "Lutron", - "model": self._device["model"], + "manufacturer": MANUFACTURER, + "model": f"{self._device['model']} ({self._device['type']})", + "via_device": (DOMAIN, self._bridge_device["serial"]), } @property diff --git a/homeassistant/components/lutron_caseta/binary_sensor.py b/homeassistant/components/lutron_caseta/binary_sensor.py index cb50cb1a6e8..97053eba08c 100644 --- a/homeassistant/components/lutron_caseta/binary_sensor.py +++ b/homeassistant/components/lutron_caseta/binary_sensor.py @@ -7,6 +7,7 @@ from homeassistant.components.binary_sensor import ( ) from . import DOMAIN as CASETA_DOMAIN, LutronCasetaDevice +from .const import BRIDGE_DEVICE, BRIDGE_LEAP async def async_setup_entry(hass, config_entry, async_add_entities): @@ -17,11 +18,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """ entities = [] - bridge = hass.data[CASETA_DOMAIN][config_entry.entry_id] + data = hass.data[CASETA_DOMAIN][config_entry.entry_id] + bridge = data[BRIDGE_LEAP] + bridge_device = data[BRIDGE_DEVICE] occupancy_groups = bridge.occupancy_groups for occupancy_group in occupancy_groups.values(): - entity = LutronOccupancySensor(occupancy_group, bridge) + entity = LutronOccupancySensor(occupancy_group, bridge, bridge_device) entities.append(entity) async_add_entities(entities, True) diff --git a/homeassistant/components/lutron_caseta/config_flow.py b/homeassistant/components/lutron_caseta/config_flow.py index 1290d88b09c..ab9865f999a 100644 --- a/homeassistant/components/lutron_caseta/config_flow.py +++ b/homeassistant/components/lutron_caseta/config_flow.py @@ -1,26 +1,47 @@ """Config flow for Lutron Caseta.""" +import asyncio import logging +import os +import ssl +import async_timeout +from pylutron_caseta.pairing import PAIR_CA, PAIR_CERT, PAIR_KEY, async_pair from pylutron_caseta.smartbridge import Smartbridge +import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_HOST +from homeassistant.components.zeroconf import ATTR_HOSTNAME +from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.core import callback -from . import DOMAIN # pylint: disable=unused-import from .const import ( ABORT_REASON_ALREADY_CONFIGURED, ABORT_REASON_CANNOT_CONNECT, + BRIDGE_TIMEOUT, CONF_CA_CERTS, CONF_CERTFILE, CONF_KEYFILE, ERROR_CANNOT_CONNECT, STEP_IMPORT_FAILED, ) +from .const import DOMAIN # pylint: disable=unused-import + +HOSTNAME = "hostname" + + +FILE_MAPPING = { + PAIR_KEY: CONF_KEYFILE, + PAIR_CERT: CONF_CERTFILE, + PAIR_CA: CONF_CA_CERTS, +} _LOGGER = logging.getLogger(__name__) ENTRY_DEFAULT_TITLE = "Caséta bridge" +DATA_SCHEMA_USER = vol.Schema({vol.Required(CONF_HOST): str}) +TLS_ASSET_TEMPLATE = "lutron_caseta-{}-{}.pem" + class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle Lutron Caseta config flow.""" @@ -31,6 +52,120 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize a Lutron Caseta flow.""" self.data = {} + self.lutron_id = None + self.tls_assets_validated = False + self.attempted_tls_validation = False + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + if user_input is not None: + self.data[CONF_HOST] = user_input[CONF_HOST] + return await self.async_step_link() + + return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA_USER) + + async def async_step_zeroconf(self, discovery_info): + """Handle a flow initialized by zeroconf discovery.""" + hostname = discovery_info[ATTR_HOSTNAME] + if hostname is None or not hostname.startswith("lutron-"): + return self.async_abort(reason="not_lutron_device") + + self.lutron_id = hostname.split("-")[1].replace(".local.", "") + + await self.async_set_unique_id(self.lutron_id) + host = discovery_info[CONF_HOST] + self._abort_if_unique_id_configured({CONF_HOST: host}) + + self.data[CONF_HOST] = host + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context["title_placeholders"] = { + CONF_NAME: self.bridge_id, + CONF_HOST: host, + } + return await self.async_step_link() + + async def async_step_homekit(self, discovery_info): + """Handle a flow initialized by homekit discovery.""" + return await self.async_step_zeroconf(discovery_info) + + async def async_step_link(self, user_input=None): + """Handle pairing with the hub.""" + errors = {} + # Abort if existing entry with matching host exists. + if self._async_data_host_is_already_configured(): + return self.async_abort(reason=ABORT_REASON_ALREADY_CONFIGURED) + + self._configure_tls_assets() + + if ( + not self.attempted_tls_validation + and await self.hass.async_add_executor_job(self._tls_assets_exist) + and await self.async_validate_connectable_bridge_config() + ): + self.tls_assets_validated = True + self.attempted_tls_validation = True + + if user_input is not None: + if self.tls_assets_validated: + # If we previous paired and the tls assets already exist, + # we do not need to go though pairing again. + return self.async_create_entry(title=self.bridge_id, data=self.data) + + assets = None + try: + assets = await async_pair(self.data[CONF_HOST]) + except (asyncio.TimeoutError, OSError): + errors["base"] = "cannot_connect" + + if not errors: + await self.hass.async_add_executor_job(self._write_tls_assets, assets) + return self.async_create_entry(title=self.bridge_id, data=self.data) + + return self.async_show_form( + step_id="link", + errors=errors, + description_placeholders={ + CONF_NAME: self.bridge_id, + CONF_HOST: self.data[CONF_HOST], + }, + ) + + @property + def bridge_id(self): + """Return the best identifier for the bridge. + + If the bridge was not discovered via zeroconf, + we fallback to using the host. + """ + return self.lutron_id or self.data[CONF_HOST] + + def _write_tls_assets(self, assets): + """Write the tls assets to disk.""" + for asset_key, conf_key in FILE_MAPPING.items(): + with open(self.hass.config.path(self.data[conf_key]), "w") as file_handle: + file_handle.write(assets[asset_key]) + + def _tls_assets_exist(self): + """Check to see if tls assets are already on disk.""" + for conf_key in FILE_MAPPING.values(): + if not os.path.exists(self.hass.config.path(self.data[conf_key])): + return False + return True + + @callback + def _configure_tls_assets(self): + """Fill the tls asset locations in self.data.""" + for asset_key, conf_key in FILE_MAPPING.items(): + self.data[conf_key] = TLS_ASSET_TEMPLATE.format(self.bridge_id, asset_key) + + @callback + def _async_data_host_is_already_configured(self): + """Check to see if the host is already configured.""" + return any( + self.data[CONF_HOST] == entry.data[CONF_HOST] + for entry in self._async_current_entries() + if CONF_HOST in entry.data + ) async def async_step_import(self, import_info): """Import a new Caseta bridge as a config entry. @@ -38,15 +173,14 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): This flow is triggered by `async_setup`. """ - # Abort if existing entry with matching host exists. host = import_info[CONF_HOST] - if any( - host == entry.data[CONF_HOST] for entry in self._async_current_entries() - ): - return self.async_abort(reason=ABORT_REASON_ALREADY_CONFIGURED) - # Store the imported config for other steps in this flow to access. self.data[CONF_HOST] = host + + # Abort if existing entry with matching host exists. + if self._async_data_host_is_already_configured(): + return self.async_abort(reason=ABORT_REASON_ALREADY_CONFIGURED) + self.data[CONF_KEYFILE] = import_info[CONF_KEYFILE] self.data[CONF_CERTFILE] = import_info[CONF_CERTFILE] self.data[CONF_CA_CERTS] = import_info[CONF_CA_CERTS] @@ -68,6 +202,9 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import_failed(self, user_input=None): """Make failed import surfaced to user.""" + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context["title_placeholders"] = {CONF_NAME: self.data[CONF_HOST]} + if user_input is None: return self.async_show_form( step_id=STEP_IMPORT_FAILED, @@ -80,6 +217,8 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def async_validate_connectable_bridge_config(self): """Check if we can connect to the bridge with the current config.""" + bridge = None + try: bridge = Smartbridge.create_tls( hostname=self.data[CONF_HOST], @@ -87,16 +226,23 @@ class LutronCasetaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): certfile=self.hass.config.path(self.data[CONF_CERTFILE]), ca_certs=self.hass.config.path(self.data[CONF_CA_CERTS]), ) - - await bridge.connect() - if not bridge.is_connected(): - return False - - await bridge.close() - return True - except Exception: # pylint: disable=broad-except - _LOGGER.exception( - "Unknown exception while checking connectivity to bridge %s", + except ssl.SSLError: + _LOGGER.error( + "Invalid certificate used to connect to bridge at %s", self.data[CONF_HOST], ) return False + + connected_ok = False + try: + async with async_timeout.timeout(BRIDGE_TIMEOUT): + await bridge.connect() + connected_ok = bridge.is_connected() + except asyncio.TimeoutError: + _LOGGER.error( + "Timeout while trying to connect to bridge at %s", + self.data[CONF_HOST], + ) + + await bridge.close() + return connected_ok diff --git a/homeassistant/components/lutron_caseta/const.py b/homeassistant/components/lutron_caseta/const.py index 11bc8bcd6fb..f8f9ee668c2 100644 --- a/homeassistant/components/lutron_caseta/const.py +++ b/homeassistant/components/lutron_caseta/const.py @@ -1,5 +1,7 @@ """Lutron Caseta constants.""" +DOMAIN = "lutron_caseta" + CONF_KEYFILE = "keyfile" CONF_CERTFILE = "certfile" CONF_CA_CERTS = "ca_certs" @@ -8,3 +10,28 @@ STEP_IMPORT_FAILED = "import_failed" ERROR_CANNOT_CONNECT = "cannot_connect" ABORT_REASON_CANNOT_CONNECT = "cannot_connect" ABORT_REASON_ALREADY_CONFIGURED = "already_configured" + +BRIDGE_LEAP = "leap" +BRIDGE_LIP = "lip" +BRIDGE_DEVICE = "bridge_device" +BUTTON_DEVICES = "button_devices" +LUTRON_CASETA_BUTTON_EVENT = "lutron_caseta_button_event" + +BRIDGE_DEVICE_ID = "1" + +MANUFACTURER = "Lutron" + +ATTR_SERIAL = "serial" +ATTR_TYPE = "type" +ATTR_BUTTON_NUMBER = "button_number" +ATTR_DEVICE_NAME = "device_name" +ATTR_AREA_NAME = "area_name" +ATTR_ACTION = "action" + +ACTION_PRESS = "press" +ACTION_RELEASE = "release" + +CONF_TYPE = "type" +CONF_SUBTYPE = "subtype" + +BRIDGE_TIMEOUT = 35 diff --git a/homeassistant/components/lutron_caseta/cover.py b/homeassistant/components/lutron_caseta/cover.py index 8db97e3fd0c..b3924ba31c8 100644 --- a/homeassistant/components/lutron_caseta/cover.py +++ b/homeassistant/components/lutron_caseta/cover.py @@ -12,7 +12,8 @@ from homeassistant.components.cover import ( CoverEntity, ) -from . import DOMAIN as CASETA_DOMAIN, LutronCasetaDevice +from . import LutronCasetaDevice +from .const import BRIDGE_DEVICE, BRIDGE_LEAP, DOMAIN as CASETA_DOMAIN _LOGGER = logging.getLogger(__name__) @@ -25,11 +26,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """ entities = [] - bridge = hass.data[CASETA_DOMAIN][config_entry.entry_id] + data = hass.data[CASETA_DOMAIN][config_entry.entry_id] + bridge = data[BRIDGE_LEAP] + bridge_device = data[BRIDGE_DEVICE] cover_devices = bridge.get_devices_by_domain(DOMAIN) for cover_device in cover_devices: - entity = LutronCasetaCover(cover_device, bridge) + entity = LutronCasetaCover(cover_device, bridge, bridge_device) entities.append(entity) async_add_entities(entities, True) diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py new file mode 100644 index 00000000000..402db7286af --- /dev/null +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -0,0 +1,296 @@ +"""Provides device triggers for lutron caseta.""" +import logging +from typing import List + +import voluptuous as vol + +from homeassistant.components.automation import AutomationActionType +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) +from homeassistant.components.homeassistant.triggers import event as event_trigger +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_EVENT, + CONF_PLATFORM, + CONF_TYPE, +) +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.typing import ConfigType + +from .const import ( + ACTION_PRESS, + ACTION_RELEASE, + ATTR_ACTION, + ATTR_BUTTON_NUMBER, + ATTR_SERIAL, + BUTTON_DEVICES, + CONF_SUBTYPE, + DOMAIN, + LUTRON_CASETA_BUTTON_EVENT, +) + +_LOGGER = logging.getLogger(__name__) + + +SUPPORTED_INPUTS_EVENTS_TYPES = [ACTION_PRESS, ACTION_RELEASE] + +LUTRON_BUTTON_TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In(SUPPORTED_INPUTS_EVENTS_TYPES), + } +) + + +PICO_2_BUTTON_BUTTON_TYPES = { + "on": 2, + "off": 4, +} +PICO_2_BUTTON_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( + { + vol.Required(CONF_SUBTYPE): vol.In(PICO_2_BUTTON_BUTTON_TYPES), + } +) + + +PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES = { + "on": 2, + "off": 4, + "raise": 5, + "lower": 6, +} +PICO_2_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( + { + vol.Required(CONF_SUBTYPE): vol.In(PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES), + } +) + + +PICO_3_BUTTON_BUTTON_TYPES = { + "on": 2, + "stop": 3, + "off": 4, +} +PICO_3_BUTTON_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( + { + vol.Required(CONF_SUBTYPE): vol.In(PICO_3_BUTTON_BUTTON_TYPES), + } +) + +PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES = { + "on": 2, + "stop": 3, + "off": 4, + "raise": 5, + "lower": 6, +} +PICO_3_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( + { + vol.Required(CONF_SUBTYPE): vol.In(PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES), + } +) + +PICO_4_BUTTON_BUTTON_TYPES = { + "button_1": 8, + "button_2": 9, + "button_3": 10, + "button_4": 11, +} +PICO_4_BUTTON_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( + { + vol.Required(CONF_SUBTYPE): vol.In(PICO_4_BUTTON_BUTTON_TYPES), + } +) + + +PICO_4_BUTTON_ZONE_BUTTON_TYPES = { + "on": 8, + "raise": 9, + "lower": 10, + "off": 11, +} +PICO_4_BUTTON_ZONE_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( + { + vol.Required(CONF_SUBTYPE): vol.In(PICO_4_BUTTON_ZONE_BUTTON_TYPES), + } +) + + +PICO_4_BUTTON_SCENE_BUTTON_TYPES = { + "button_1": 8, + "button_2": 9, + "button_3": 10, + "off": 11, +} +PICO_4_BUTTON_SCENE_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( + { + vol.Required(CONF_SUBTYPE): vol.In(PICO_4_BUTTON_SCENE_BUTTON_TYPES), + } +) + + +PICO_4_BUTTON_2_GROUP_BUTTON_TYPES = { + "group_1_button_1": 8, + "group_1_button_2": 9, + "group_2_button_1": 10, + "group_2_button_2": 11, +} +PICO_4_BUTTON_2_GROUP_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( + { + vol.Required(CONF_SUBTYPE): vol.In(PICO_4_BUTTON_2_GROUP_BUTTON_TYPES), + } +) + +FOUR_GROUP_REMOTE_BUTTON_TYPES = { + "open_all": 2, + "stop_all": 3, + "close_all": 4, + "raise_all": 5, + "lower_all": 6, + "open_1": 10, + "stop_1": 11, + "close_1": 12, + "raise_1": 13, + "lower_1": 14, + "open_2": 18, + "stop_2": 19, + "close_2": 20, + "raise_2": 21, + "lower_2": 22, + "open_3": 26, + "stop_3": 27, + "close_3": 28, + "raise_3": 29, + "lower_3": 30, + "open_4": 34, + "stop_4": 35, + "close_4": 36, + "raise_4": 37, + "lower_4": 38, +} +FOUR_GROUP_REMOTE_TRIGGER_SCHEMA = LUTRON_BUTTON_TRIGGER_SCHEMA.extend( + { + vol.Required(CONF_SUBTYPE): vol.In(FOUR_GROUP_REMOTE_BUTTON_TYPES), + } +) + +DEVICE_TYPE_SCHEMA_MAP = { + "Pico2Button": PICO_2_BUTTON_TRIGGER_SCHEMA, + "Pico2ButtonRaiseLower": PICO_2_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA, + "Pico3Button": PICO_3_BUTTON_TRIGGER_SCHEMA, + "Pico3ButtonRaiseLower": PICO_3_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA, + "Pico4Button": PICO_4_BUTTON_TRIGGER_SCHEMA, + "Pico4ButtonScene": PICO_4_BUTTON_SCENE_TRIGGER_SCHEMA, + "Pico4ButtonZone": PICO_4_BUTTON_ZONE_TRIGGER_SCHEMA, + "Pico4Button2Group": PICO_4_BUTTON_2_GROUP_TRIGGER_SCHEMA, + "FourGroupRemote": FOUR_GROUP_REMOTE_TRIGGER_SCHEMA, +} + +DEVICE_TYPE_SUBTYPE_MAP = { + "Pico2Button": PICO_2_BUTTON_BUTTON_TYPES, + "Pico2ButtonRaiseLower": PICO_2_BUTTON_RAISE_LOWER_BUTTON_TYPES, + "Pico3Button": PICO_3_BUTTON_BUTTON_TYPES, + "Pico3ButtonRaiseLower": PICO_3_BUTTON_RAISE_LOWER_BUTTON_TYPES, + "Pico4Button": PICO_4_BUTTON_BUTTON_TYPES, + "Pico4ButtonScene": PICO_4_BUTTON_SCENE_BUTTON_TYPES, + "Pico4ButtonZone": PICO_4_BUTTON_ZONE_BUTTON_TYPES, + "Pico4Button2Group": PICO_4_BUTTON_2_GROUP_BUTTON_TYPES, + "FourGroupRemote": FOUR_GROUP_REMOTE_BUTTON_TYPES, +} + +TRIGGER_SCHEMA = vol.Any( + PICO_2_BUTTON_TRIGGER_SCHEMA, + PICO_3_BUTTON_RAISE_LOWER_TRIGGER_SCHEMA, + PICO_4_BUTTON_TRIGGER_SCHEMA, + PICO_4_BUTTON_SCENE_TRIGGER_SCHEMA, + PICO_4_BUTTON_ZONE_TRIGGER_SCHEMA, + PICO_4_BUTTON_2_GROUP_TRIGGER_SCHEMA, + FOUR_GROUP_REMOTE_TRIGGER_SCHEMA, +) + + +async def async_validate_trigger_config(hass: HomeAssistant, config: ConfigType): + """Validate config.""" + # if device is available verify parameters against device capabilities + device = get_button_device_by_dr_id(hass, config[CONF_DEVICE_ID]) + + if not device: + return config + + schema = DEVICE_TYPE_SCHEMA_MAP.get(device["type"]) + + if not schema: + raise InvalidDeviceAutomationConfig( + f"Device type {device['type']} not supported: {config[CONF_DEVICE_ID]}" + ) + + return schema(config) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers for lutron caseta devices.""" + triggers = [] + + device = get_button_device_by_dr_id(hass, device_id) + if not device: + raise InvalidDeviceAutomationConfig(f"Device not found: {device_id}") + + valid_buttons = DEVICE_TYPE_SUBTYPE_MAP.get(device["type"], []) + + for trigger in SUPPORTED_INPUTS_EVENTS_TYPES: + for subtype in valid_buttons: + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: trigger, + CONF_SUBTYPE: subtype, + } + ) + + return triggers + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + device = get_button_device_by_dr_id(hass, config[CONF_DEVICE_ID]) + schema = DEVICE_TYPE_SCHEMA_MAP.get(device["type"]) + valid_buttons = DEVICE_TYPE_SUBTYPE_MAP.get(device["type"]) + config = schema(config) + event_config = event_trigger.TRIGGER_SCHEMA( + { + event_trigger.CONF_PLATFORM: CONF_EVENT, + event_trigger.CONF_EVENT_TYPE: LUTRON_CASETA_BUTTON_EVENT, + event_trigger.CONF_EVENT_DATA: { + ATTR_SERIAL: device["serial"], + ATTR_BUTTON_NUMBER: valid_buttons[config[CONF_SUBTYPE]], + ATTR_ACTION: config[CONF_TYPE], + }, + } + ) + event_config = event_trigger.TRIGGER_SCHEMA(event_config) + return await event_trigger.async_attach_trigger( + hass, event_config, action, automation_info, platform_type="device" + ) + + +def get_button_device_by_dr_id(hass: HomeAssistant, device_id: str): + """Get a lutron device for the given device id.""" + if DOMAIN not in hass.data: + return None + + for config_entry in hass.data[DOMAIN]: + button_devices = hass.data[DOMAIN][config_entry][BUTTON_DEVICES] + device = button_devices.get(device_id) + if device: + return device + + return None diff --git a/homeassistant/components/lutron_caseta/fan.py b/homeassistant/components/lutron_caseta/fan.py index 90728c5f2fe..045cd35cd17 100644 --- a/homeassistant/components/lutron_caseta/fan.py +++ b/homeassistant/components/lutron_caseta/fan.py @@ -13,7 +13,8 @@ from homeassistant.components.fan import ( FanEntity, ) -from . import DOMAIN as CASETA_DOMAIN, LutronCasetaDevice +from . import LutronCasetaDevice +from .const import BRIDGE_DEVICE, BRIDGE_LEAP, DOMAIN as CASETA_DOMAIN _LOGGER = logging.getLogger(__name__) @@ -44,11 +45,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """ entities = [] - bridge = hass.data[CASETA_DOMAIN][config_entry.entry_id] + data = hass.data[CASETA_DOMAIN][config_entry.entry_id] + bridge = data[BRIDGE_LEAP] + bridge_device = data[BRIDGE_DEVICE] fan_devices = bridge.get_devices_by_domain(DOMAIN) for fan_device in fan_devices: - entity = LutronCasetaFan(fan_device, bridge) + entity = LutronCasetaFan(fan_device, bridge, bridge_device) entities.append(entity) async_add_entities(entities, True) diff --git a/homeassistant/components/lutron_caseta/light.py b/homeassistant/components/lutron_caseta/light.py index c46e8931390..ec200118082 100644 --- a/homeassistant/components/lutron_caseta/light.py +++ b/homeassistant/components/lutron_caseta/light.py @@ -11,7 +11,8 @@ from homeassistant.components.light import ( LightEntity, ) -from . import DOMAIN as CASETA_DOMAIN, LutronCasetaDevice +from . import LutronCasetaDevice +from .const import BRIDGE_DEVICE, BRIDGE_LEAP, DOMAIN as CASETA_DOMAIN _LOGGER = logging.getLogger(__name__) @@ -34,11 +35,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """ entities = [] - bridge = hass.data[CASETA_DOMAIN][config_entry.entry_id] + data = hass.data[CASETA_DOMAIN][config_entry.entry_id] + bridge = data[BRIDGE_LEAP] + bridge_device = data[BRIDGE_DEVICE] light_devices = bridge.get_devices_by_domain(DOMAIN) for light_device in light_devices: - entity = LutronCasetaLight(light_device, bridge) + entity = LutronCasetaLight(light_device, bridge, bridge_device) entities.append(entity) async_add_entities(entities, True) diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index b21cfed30c2..34ab75dc0cd 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -3,9 +3,12 @@ "name": "Lutron Caséta", "documentation": "https://www.home-assistant.io/integrations/lutron_caseta", "requirements": [ - "pylutron-caseta==0.7.2" + "pylutron-caseta==0.9.0", "aiolip==1.0.1" ], - "codeowners": [ - "@swails" - ] -} \ No newline at end of file + "config_flow": true, + "zeroconf": ["_leap._tcp.local."], + "homekit": { + "models": ["Smart Bridge"] + }, + "codeowners": ["@swails", "@bdraco"] +} diff --git a/homeassistant/components/lutron_caseta/scene.py b/homeassistant/components/lutron_caseta/scene.py index 51dff935d93..d70048db8cd 100644 --- a/homeassistant/components/lutron_caseta/scene.py +++ b/homeassistant/components/lutron_caseta/scene.py @@ -3,7 +3,7 @@ from typing import Any from homeassistant.components.scene import Scene -from . import DOMAIN as CASETA_DOMAIN +from .const import BRIDGE_LEAP, DOMAIN as CASETA_DOMAIN async def async_setup_entry(hass, config_entry, async_add_entities): @@ -14,7 +14,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """ entities = [] - bridge = hass.data[CASETA_DOMAIN][config_entry.entry_id] + data = hass.data[CASETA_DOMAIN][config_entry.entry_id] + bridge = data[BRIDGE_LEAP] scenes = bridge.get_scenes() for scene in scenes: diff --git a/homeassistant/components/lutron_caseta/strings.json b/homeassistant/components/lutron_caseta/strings.json index a03cdd8c9d6..bdaec22e776 100644 --- a/homeassistant/components/lutron_caseta/strings.json +++ b/homeassistant/components/lutron_caseta/strings.json @@ -1,17 +1,76 @@ { "config": { + "flow_title": "Lutron Caséta {name} ({host})", "step": { "import_failed": { "title": "Failed to import Caséta bridge configuration.", "description": "Couldn’t setup bridge (host: {host}) imported from configuration.yaml." + }, + "user": { + "title": "Automaticlly connect to the bridge", + "description": "Enter the ip address of the device.", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "link": { + "title": "Pair with the bridge", + "description": "To pair with {name} ({host}), after submitting this form, press the black button on the back of the bridge." } }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" }, "abort": { + "not_lutron_device": "Discovered device is not a Lutron device", "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "First button", + "button_2": "Second button", + "button_3": "Third button", + "button_4": "Fourth button", + "group_1_button_1": "First Group first button", + "group_1_button_2": "First Group second button", + "group_2_button_1": "Second Group first button", + "group_2_button_2": "Second Group second button", + "on": "On", + "stop": "Stop (favorite)", + "off": "Off", + "raise": "Raise", + "lower": "Lower", + "open_all": "Open all", + "stop_all": "Stop all", + "close_all": "Close all", + "raise_all": "Raise all", + "lower_all": "Lower all", + "open_1": "Open 1", + "stop_1": "Stop 1", + "close_1": "Close 1", + "raise_1": "Raise 1", + "lower_1": "Lower 1", + "open_2": "Open 2", + "stop_2": "Stop 2", + "close_2": "Close 2", + "raise_2": "Raise 2", + "lower_2": "Lower 2", + "open_3": "Open 3", + "stop_3": "Stop 3", + "close_3": "Close 3", + "raise_3": "Raise 3", + "lower_3": "Lower 3", + "open_4": "Open 4", + "stop_4": "Stop 4", + "close_4": "Close 4", + "raise_4": "Raise 4", + "lower_4": "Lower 4" + }, + "trigger_type": { + "press": "\"{subtype}\" pressed", + "release": "\"{subtype}\" released" + } } } diff --git a/homeassistant/components/lutron_caseta/switch.py b/homeassistant/components/lutron_caseta/switch.py index 1cccd485524..1e5b4ab6fe5 100644 --- a/homeassistant/components/lutron_caseta/switch.py +++ b/homeassistant/components/lutron_caseta/switch.py @@ -3,7 +3,8 @@ import logging from homeassistant.components.switch import DOMAIN, SwitchEntity -from . import DOMAIN as CASETA_DOMAIN, LutronCasetaDevice +from . import LutronCasetaDevice +from .const import BRIDGE_DEVICE, BRIDGE_LEAP, DOMAIN as CASETA_DOMAIN _LOGGER = logging.getLogger(__name__) @@ -16,11 +17,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """ entities = [] - bridge = hass.data[CASETA_DOMAIN][config_entry.entry_id] + data = hass.data[CASETA_DOMAIN][config_entry.entry_id] + bridge = data[BRIDGE_LEAP] + bridge_device = data[BRIDGE_DEVICE] switch_devices = bridge.get_devices_by_domain(DOMAIN) for switch_device in switch_devices: - entity = LutronCasetaLight(switch_device, bridge) + entity = LutronCasetaLight(switch_device, bridge, bridge_device) entities.append(entity) async_add_entities(entities, True) diff --git a/homeassistant/components/lutron_caseta/translations/ca.json b/homeassistant/components/lutron_caseta/translations/ca.json index c3b0e686cc4..5f2cc5d4087 100644 --- a/homeassistant/components/lutron_caseta/translations/ca.json +++ b/homeassistant/components/lutron_caseta/translations/ca.json @@ -2,16 +2,75 @@ "config": { "abort": { "already_configured": "El dispositiu ja est\u00e0 configurat", - "cannot_connect": "Ha fallat la connexi\u00f3" + "cannot_connect": "Ha fallat la connexi\u00f3", + "not_lutron_device": "El dispositiu descobert no \u00e9s un dispositiu Lutron" }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3" }, + "flow_title": "Lutron Cas\u00e9ta {name} ({host})", "step": { "import_failed": { "description": "No s'ha pogut configurar l'enlla\u00e7 (amfitri\u00f3: {host}) importat de configuration.yaml.", "title": "No s'ha pogut importar la configuraci\u00f3 de l'enlla\u00e7 de Cas\u00e9ta." + }, + "link": { + "description": "Per a vincular amb {name} ({host}), despr\u00e9s d'enviar aquest formulari, prem el bot\u00f3 negre de la part posterior de l'enlla\u00e7.", + "title": "Vinculaci\u00f3 amb enlla\u00e7" + }, + "user": { + "data": { + "host": "Amfitri\u00f3" + }, + "description": "Introdueix l'adre\u00e7a IP del dispositiu.", + "title": "Connexi\u00f3 autom\u00e0tica amb l'enlla\u00e7" } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Primer bot\u00f3", + "button_2": "Segon bot\u00f3", + "button_3": "Tercer bot\u00f3", + "button_4": "Quart bot\u00f3", + "close_1": "Tanca 1", + "close_2": "Tanca 2", + "close_3": "Tanca 3", + "close_4": "Tanca 4", + "close_all": "Tanca-ho tot", + "group_1_button_1": "Primer bot\u00f3 del primer grup", + "group_1_button_2": "Segon bot\u00f3 del primer grup", + "group_2_button_1": "Primer bot\u00f3 del segon grup", + "group_2_button_2": "Segon bot\u00f3 del segon grup", + "lower": "Baixa", + "lower_1": "Baixa 1", + "lower_2": "Baixa 2", + "lower_3": "Baixa 3", + "lower_4": "Baixa 4", + "lower_all": "Baixa-ho tot", + "off": "OFF", + "on": "ON", + "open_1": "Obre 1", + "open_2": "Obre 2", + "open_3": "Obre 3", + "open_4": "Obre 4", + "open_all": "Obre-ho tot", + "raise": "Puja", + "raise_1": "Puja 1", + "raise_2": "Puja 2", + "raise_3": "Puja 3", + "raise_4": "Puja 4", + "raise_all": "Puja-ho tot", + "stop": "Atura (preferit)", + "stop_1": "Atura 1", + "stop_2": "Atura 2", + "stop_3": "Atura 3", + "stop_4": "Atura 4", + "stop_all": "Atura-ho tot" + }, + "trigger_type": { + "press": "\"{subtype}\" premut", + "release": "\"{subtype}\" alliberat" + } } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/cs.json b/homeassistant/components/lutron_caseta/translations/cs.json index 60fa7fddced..4ccfa17e6d3 100644 --- a/homeassistant/components/lutron_caseta/translations/cs.json +++ b/homeassistant/components/lutron_caseta/translations/cs.json @@ -6,6 +6,13 @@ }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "host": "Hostitel" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/en.json b/homeassistant/components/lutron_caseta/translations/en.json index 3797d476db3..8ea0672a3f3 100644 --- a/homeassistant/components/lutron_caseta/translations/en.json +++ b/homeassistant/components/lutron_caseta/translations/en.json @@ -2,16 +2,75 @@ "config": { "abort": { "already_configured": "Device is already configured", - "cannot_connect": "Failed to connect" + "cannot_connect": "Failed to connect", + "not_lutron_device": "Discovered device is not a Lutron device" }, "error": { "cannot_connect": "Failed to connect" }, + "flow_title": "Lutron Cas\u00e9ta {name} ({host})", "step": { "import_failed": { "description": "Couldn\u2019t setup bridge (host: {host}) imported from configuration.yaml.", "title": "Failed to import Cas\u00e9ta bridge configuration." + }, + "link": { + "description": "To pair with {name} ({host}), after submitting this form, press the black button on the back of the bridge.", + "title": "Pair with the bridge" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Enter the ip address of the device.", + "title": "Automaticlly connect to the bridge" } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "First button", + "button_2": "Second button", + "button_3": "Third button", + "button_4": "Fourth button", + "close_1": "Close 1", + "close_2": "Close 2", + "close_3": "Close 3", + "close_4": "Close 4", + "close_all": "Close all", + "group_1_button_1": "First Group first button", + "group_1_button_2": "First Group second button", + "group_2_button_1": "Second Group first button", + "group_2_button_2": "Second Group second button", + "lower": "Lower", + "lower_1": "Lower 1", + "lower_2": "Lower 2", + "lower_3": "Lower 3", + "lower_4": "Lower 4", + "lower_all": "Lower all", + "off": "Off", + "on": "On", + "open_1": "Open 1", + "open_2": "Open 2", + "open_3": "Open 3", + "open_4": "Open 4", + "open_all": "Open all", + "raise": "Raise", + "raise_1": "Raise 1", + "raise_2": "Raise 2", + "raise_3": "Raise 3", + "raise_4": "Raise 4", + "raise_all": "Raise all", + "stop": "Stop (favorite)", + "stop_1": "Stop 1", + "stop_2": "Stop 2", + "stop_3": "Stop 3", + "stop_4": "Stop 4", + "stop_all": "Stop all" + }, + "trigger_type": { + "press": "\"{subtype}\" pressed", + "release": "\"{subtype}\" released" + } } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/es.json b/homeassistant/components/lutron_caseta/translations/es.json index cfd8551bab9..37b1a0d9072 100644 --- a/homeassistant/components/lutron_caseta/translations/es.json +++ b/homeassistant/components/lutron_caseta/translations/es.json @@ -2,16 +2,50 @@ "config": { "abort": { "already_configured": "El dispositivo ya est\u00e1 configurado", - "cannot_connect": "No se pudo conectar" + "cannot_connect": "No se pudo conectar", + "not_lutron_device": "El dispositivo descubierto no es un dispositivo de Lutron" }, "error": { "cannot_connect": "No se pudo conectar" }, + "flow_title": "Lutron Cas\u00e9ta {name} ({host})", "step": { "import_failed": { "description": "No se puede configurar bridge (host: {host}) importado desde configuration.yaml.", "title": "Error al importar la configuraci\u00f3n del bridge Cas\u00e9ta." + }, + "link": { + "description": "Para emparejar con {name} ({host}), despu\u00e9s de enviar este formulario, presione el bot\u00f3n negro en la parte posterior del puente.", + "title": "Emparejar con el puente" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Introduzca la direcci\u00f3n ip del dispositivo.", + "title": "Conectar autom\u00e1ticamente con el dispositivo" } } + }, + "device_automation": { + "trigger_subtype": { + "open_1": "Abrir 1", + "open_2": "Abrir 2", + "open_3": "Abrir 3", + "open_4": "Abrir 4", + "open_all": "Abrir todo", + "raise": "Levantar", + "raise_1": "Levantar 1", + "raise_2": "Levantar 2", + "raise_3": "Levantar 3", + "raise_4": "Levantar 4", + "raise_all": "Levantar todo", + "stop": "Detener (favorito)", + "stop_1": "Detener 1", + "stop_2": "Detener 2", + "stop_3": "Detener 3", + "stop_4": "Detener 4", + "stop_all": "Detener todo" + } } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/et.json b/homeassistant/components/lutron_caseta/translations/et.json index ed352c7bcc4..81fee6d5b4a 100644 --- a/homeassistant/components/lutron_caseta/translations/et.json +++ b/homeassistant/components/lutron_caseta/translations/et.json @@ -2,16 +2,75 @@ "config": { "abort": { "already_configured": "Seade on juba h\u00e4\u00e4lestatud", - "cannot_connect": "\u00dchendamine nurjus" + "cannot_connect": "\u00dchendamine nurjus", + "not_lutron_device": "Avastatud seade ei ole Lutroni seade" }, "error": { "cannot_connect": "\u00dchendamine nurjus" }, + "flow_title": "Lutron Cas\u00e9ta {name} ( {host} )", "step": { "import_failed": { "description": "Silla (host: {host} ) seadistamine configuration.yaml kirje teabest nurjus.", "title": "Cas\u00e9ta Bridge seadete importimine nurjus." + }, + "link": { + "description": "{name} ({host}) sidumiseks vajuta p\u00e4rast selle vormi esitamist silla tagak\u00fcljel olevat musta nuppu.", + "title": "Sillaga sidumine" + }, + "user": { + "data": { + "host": "" + }, + "description": "Sisesta seadme IP-aadress.", + "title": "\u00dchendu sillaga automaatselt" } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Esimene nupp", + "button_2": "Teine nupp", + "button_3": "Kolmas nupp", + "button_4": "Neljas nupp", + "close_1": "Sule #1", + "close_2": "Sule #2", + "close_3": "Sule #3", + "close_4": "Sule #4", + "close_all": "Sulge k\u00f5ik", + "group_1_button_1": "Esimese r\u00fchma esimene nupp", + "group_1_button_2": "Esimene r\u00fchma teine nupp", + "group_2_button_1": "Teise r\u00fchma esimene nupp", + "group_2_button_2": "Teise r\u00fchma teine nupp", + "lower": "Langeta", + "lower_1": "Langeta #1", + "lower_2": "Langeta #2", + "lower_3": "Langeta #3", + "lower_4": "Langeta #4", + "lower_all": "Langeta k\u00f5ik", + "off": "V\u00e4ljas", + "on": "Sees", + "open_1": "Ava #1", + "open_2": "Ava #2", + "open_3": "Ava #3", + "open_4": "Ava #4", + "open_all": "Ava k\u00f5ik", + "raise": "T\u00f5sta", + "raise_1": "T\u00f5sta #1", + "raise_2": "T\u00f5sta #2", + "raise_3": "T\u00f5sta #3", + "raise_4": "T\u00f5sta #4", + "raise_all": "T\u00f5sta k\u00f5ik", + "stop": "Peata lemmikasendis", + "stop_1": "Peata #1", + "stop_2": "Peata #2", + "stop_3": "Peata #3", + "stop_4": "Peata #4", + "stop_all": "Peata k\u00f5ik" + }, + "trigger_type": { + "press": "vajutati \" {subtype} \"", + "release": "\" {subtype} \" vabastati" + } } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/it.json b/homeassistant/components/lutron_caseta/translations/it.json index 5bdcf87607d..d1b3b754812 100644 --- a/homeassistant/components/lutron_caseta/translations/it.json +++ b/homeassistant/components/lutron_caseta/translations/it.json @@ -2,16 +2,75 @@ "config": { "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "cannot_connect": "Impossibile connettersi" + "cannot_connect": "Impossibile connettersi", + "not_lutron_device": "Il dispositivo rilevato non \u00e8 un dispositivo Lutron" }, "error": { "cannot_connect": "Impossibile connettersi" }, + "flow_title": "Lutron Cas\u00e9ta {name} ({host})", "step": { "import_failed": { "description": "Impossibile impostare il bridge (host: {host}) importato da configuration.yaml.", "title": "Impossibile importare la configurazione del bridge Cas\u00e9ta." + }, + "link": { + "description": "Per eseguire l'associazione con {name} ({host}), dopo aver inviato questo modulo, premere il pulsante nero sul retro del bridge.", + "title": "Associa con il bridge" + }, + "user": { + "data": { + "host": "Host" + }, + "description": "Immettere l'indirizzo IP del dispositivo.", + "title": "Connetti automaticamente al bridge" } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Primo pulsante", + "button_2": "Secondo pulsante", + "button_3": "Terzo pulsante", + "button_4": "Quarto pulsante", + "close_1": "Chiudi 1", + "close_2": "Chiudi 2", + "close_3": "Chiudi 3", + "close_4": "Chiudi 4", + "close_all": "Chiudi tutti", + "group_1_button_1": "Primo Gruppo primo pulsante", + "group_1_button_2": "Primo Gruppo secondo pulsante", + "group_2_button_1": "Secondo Gruppo primo pulsante", + "group_2_button_2": "Secondo Gruppo secondo pulsante", + "lower": "Abbassa", + "lower_1": "Abbassa 1", + "lower_2": "Abbassa 2", + "lower_3": "Abbassa 3", + "lower_4": "Abbassa 4", + "lower_all": "Abbassa tutti", + "off": "Spento", + "on": "Acceso", + "open_1": "Apri 1", + "open_2": "Apri 2", + "open_3": "Apri 3", + "open_4": "Apri 4", + "open_all": "Apri tutti", + "raise": "Alza", + "raise_1": "Alza 1", + "raise_2": "Alza 2", + "raise_3": "Alza 3", + "raise_4": "Alza 4", + "raise_all": "Alza tutti", + "stop": "Ferma (preferito)", + "stop_1": "Ferma 1", + "stop_2": "Ferma 2", + "stop_3": "Ferma 3", + "stop_4": "Ferma 4", + "stop_all": "Fermare tutti" + }, + "trigger_type": { + "press": "\"{subtype}\" premuto", + "release": "\"{subtype}\" rilasciato" + } } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/no.json b/homeassistant/components/lutron_caseta/translations/no.json index 7afac9c51a5..477370100af 100644 --- a/homeassistant/components/lutron_caseta/translations/no.json +++ b/homeassistant/components/lutron_caseta/translations/no.json @@ -2,16 +2,75 @@ "config": { "abort": { "already_configured": "Enheten er allerede konfigurert", - "cannot_connect": "Tilkobling mislyktes" + "cannot_connect": "Tilkobling mislyktes", + "not_lutron_device": "Oppdaget enhet er ikke en Lutron-enhet" }, "error": { "cannot_connect": "Tilkobling mislyktes" }, + "flow_title": "Lutron Cas\u00e9ta {name} ({host})", "step": { "import_failed": { "description": "Kunne ikke konfigurere bridge (host: {host} ) importert fra configuration.yaml.", "title": "Kan ikke importere Cas\u00e9ta bridge-konfigurasjon." + }, + "link": { + "description": "Hvis du vil pare med {name} ({host}), trykker du den svarte knappen p\u00e5 baksiden av broen etter at du har sendt dette skjemaet.", + "title": "Par med broen" + }, + "user": { + "data": { + "host": "Vert" + }, + "description": "Skriv inn ip-adressen til enheten.", + "title": "Koble automatisk til broen" } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "F\u00f8rste knapp", + "button_2": "Andre knapp", + "button_3": "Tredje knapp", + "button_4": "Fjerde knapp", + "close_1": "Lukk 1", + "close_2": "Lukk 2", + "close_3": "Lukk 3", + "close_4": "Lukk 4", + "close_all": "Lukk alle", + "group_1_button_1": "F\u00f8rste gruppe f\u00f8rste knapp", + "group_1_button_2": "F\u00f8rste gruppe andre knapp", + "group_2_button_1": "Andre gruppe f\u00f8rste knapp", + "group_2_button_2": "Andre gruppeknapp", + "lower": "Senk", + "lower_1": "Senk 1", + "lower_2": "Senk 2", + "lower_3": "Senk 3", + "lower_4": "Senk 4", + "lower_all": "Senk alle", + "off": "Av", + "on": "P\u00e5", + "open_1": "\u00c5pne 1", + "open_2": "\u00c5pne 2", + "open_3": "\u00c5pne 3", + "open_4": "\u00c5pne 4", + "open_all": "\u00c5pne alle", + "raise": "Hev", + "raise_1": "Hev 1", + "raise_2": "Hev 2", + "raise_3": "Hev 3", + "raise_4": "Hev 4", + "raise_all": "Hev alle", + "stop": "Stopp (favoritt)", + "stop_1": "Stopp 1", + "stop_2": "Stopp 2", + "stop_3": "Stopp 3", + "stop_4": "Stopp 4", + "stop_all": "Stopp alle" + }, + "trigger_type": { + "press": "\"{subtype}\" trykket", + "release": "\"{subtype}\" utgitt" + } } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/pl.json b/homeassistant/components/lutron_caseta/translations/pl.json index 07417b0149e..8a8c0a759b0 100644 --- a/homeassistant/components/lutron_caseta/translations/pl.json +++ b/homeassistant/components/lutron_caseta/translations/pl.json @@ -2,16 +2,75 @@ "config": { "abort": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "not_lutron_device": "Wykryte urz\u0105dzenie nie jest urz\u0105dzeniem Lutron" }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, + "flow_title": "Lutron Cas\u00e9ta {name} ({host})", "step": { "import_failed": { "description": "Nie mo\u017cna skonfigurowa\u0107 mostka (host: {host}) zaimportowanego z pliku configuration.yaml.", "title": "Nie uda\u0142o si\u0119 zaimportowa\u0107 konfiguracji mostka Cas\u00e9ta." + }, + "link": { + "description": "Aby sparowa\u0107 z {name} ({host}), po przes\u0142aniu tego formularza naci\u015bnij czarny przycisk z ty\u0142u mostka.", + "title": "Sparuj z mostkiem" + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + }, + "description": "Wprowad\u017a adres IP urz\u0105dzenia", + "title": "Po\u0142\u0105cz si\u0119 automatycznie z mostkiem" } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "pierwszy", + "button_2": "drugi", + "button_3": "trzeci", + "button_4": "czwarty", + "close_1": "zamknij 1", + "close_2": "zamknij 2", + "close_3": "zamknij 3", + "close_4": "zamknij 4", + "close_all": "zamknij wszystkie", + "group_1_button_1": "pierwsza grupa pierwszy przycisk", + "group_1_button_2": "pierwsza grupa drugi przycisk", + "group_2_button_1": "druga grupa pierwszy przycisk", + "group_2_button_2": "druga grupa drugi przycisk", + "lower": "opu\u015b\u0107", + "lower_1": "opu\u015b\u0107 1", + "lower_2": "opu\u015b\u0107 2", + "lower_3": "opu\u015b\u0107 3", + "lower_4": "opu\u015b\u0107 4", + "lower_all": "opu\u015b\u0107 wszystkie", + "off": "wy\u0142\u0105cz", + "on": "w\u0142\u0105cz", + "open_1": "otw\u00f3rz 1", + "open_2": "otw\u00f3rz 2", + "open_3": "otw\u00f3rz 3", + "open_4": "otw\u00f3rz 4", + "open_all": "otw\u00f3rz wszystkie", + "raise": "podnie\u015b", + "raise_1": "podnie\u015b 1", + "raise_2": "podnie\u015b 2", + "raise_3": "podnie\u015b 3", + "raise_4": "podnie\u015b 4", + "raise_all": "podnie\u015b wszystkie", + "stop": "zatrzymaj (ulubione)", + "stop_1": "zatrzymaj 1", + "stop_2": "zatrzymaj 2", + "stop_3": "zatrzymaj 3", + "stop_4": "zatrzymaj 4", + "stop_all": "zatrzymaj wszystkie" + }, + "trigger_type": { + "press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty", + "release": "przycisk \"{subtype}\" zostanie zwolniony" + } } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/ru.json b/homeassistant/components/lutron_caseta/translations/ru.json index 05bd4f51c70..edda7af8e9a 100644 --- a/homeassistant/components/lutron_caseta/translations/ru.json +++ b/homeassistant/components/lutron_caseta/translations/ru.json @@ -2,16 +2,56 @@ "config": { "abort": { "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "not_lutron_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 Lutron." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, + "flow_title": "Lutron Cas\u00e9ta {name} ({host})", "step": { "import_failed": { "description": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0448\u043b\u044e\u0437 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 configuration.yaml (\u0445\u043e\u0441\u0442: {host}).", "title": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0448\u043b\u044e\u0437\u0430." + }, + "link": { + "description": "\u0427\u0442\u043e\u0431\u044b \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441 {name} ({host}), \u043f\u043e\u0441\u043b\u0435 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0438 \u044d\u0442\u043e\u0439 \u0444\u043e\u0440\u043c\u044b \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u0447\u0435\u0440\u043d\u0443\u044e \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0437\u0430\u0434\u043d\u0435\u0439 \u0441\u0442\u043e\u0440\u043e\u043d\u0435 \u0448\u043b\u044e\u0437\u0430.", + "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0441\u043e \u0448\u043b\u044e\u0437\u043e\u043c" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 IP-\u0430\u0434\u0440\u0435\u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430.", + "title": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0442\u044c\u0441\u044f \u043a \u0448\u043b\u044e\u0437\u0443" } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "\u041f\u0435\u0440\u0432\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_2": "\u0412\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "close_1": "\u0417\u0430\u043a\u0440\u044b\u0442\u044c 1", + "close_2": "\u0417\u0430\u043a\u0440\u044b\u0442\u044c 2", + "close_3": "\u0417\u0430\u043a\u0440\u044b\u0442\u044c 3", + "close_4": "\u0417\u0430\u043a\u0440\u044b\u0442\u044c 4", + "close_all": "\u0417\u0430\u043a\u0440\u044b\u0442\u044c \u0432\u0441\u0435", + "group_1_button_1": "\u041f\u0435\u0440\u0432\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430 \u043f\u0435\u0440\u0432\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "group_1_button_2": "\u041f\u0435\u0440\u0432\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430 \u0432\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "group_2_button_1": "\u0412\u0442\u043e\u0440\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430 \u043f\u0435\u0440\u0432\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "group_2_button_2": "\u0412\u0442\u043e\u0440\u0430\u044f \u0433\u0440\u0443\u043f\u043f\u0430 \u0432\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "stop": "\u041e\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c (\u043b\u044e\u0431\u0438\u043c\u0430\u044f)", + "stop_1": "\u041e\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c 1", + "stop_2": "\u041e\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c 2", + "stop_3": "\u041e\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c 3", + "stop_4": "\u041e\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c 4", + "stop_all": "\u041e\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0432\u0441\u0435" + }, + "trigger_type": { + "press": "\"{subtype}\" \u043d\u0430\u0436\u0430\u0442\u0430", + "release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0438\u044f" + } } } \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/tr.json b/homeassistant/components/lutron_caseta/translations/tr.json new file mode 100644 index 00000000000..fdc5e71a7ac --- /dev/null +++ b/homeassistant/components/lutron_caseta/translations/tr.json @@ -0,0 +1,72 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "not_lutron_device": "Bulunan cihaz bir Lutron cihaz\u0131 de\u011fil" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "flow_title": "Lutron Cas\u00e9ta {name} ( {host} )", + "step": { + "link": { + "description": "{name} ( {host} ) ile e\u015fle\u015ftirmek i\u00e7in, bu formu g\u00f6nderdikten sonra k\u00f6pr\u00fcn\u00fcn arkas\u0131ndaki siyah d\u00fc\u011fmeye bas\u0131n.", + "title": "K\u00f6pr\u00fc ile e\u015fle\u015ftirin" + }, + "user": { + "data": { + "host": "Ana Bilgisayar" + }, + "description": "Cihaz\u0131n ip adresini girin.", + "title": "K\u00f6pr\u00fcye otomatik olarak ba\u011flan\u0131n" + } + } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "\u0130lk d\u00fc\u011fme", + "button_2": "\u0130kinci d\u00fc\u011fme", + "button_3": "\u00dc\u00e7\u00fcnc\u00fc d\u00fc\u011fme", + "button_4": "D\u00f6rd\u00fcnc\u00fc d\u00fc\u011fme", + "close_1": "Kapat 1", + "close_2": "Kapat 2", + "close_3": "Kapat 3", + "close_4": "Kapat 4", + "close_all": "Hepsini kapat", + "group_1_button_1": "Birinci Grup ilk d\u00fc\u011fme", + "group_1_button_2": "Birinci Grup ikinci d\u00fc\u011fme", + "group_2_button_1": "\u0130kinci Grup birinci d\u00fc\u011fme", + "group_2_button_2": "\u0130kinci Grup ikinci d\u00fc\u011fme", + "lower": "Alt", + "lower_1": "Alt 1", + "lower_2": "Alt 2", + "lower_3": "Alt 3", + "lower_4": "Alt 4", + "lower_all": "Hepsini indir", + "off": "Kapal\u0131", + "on": "A\u00e7\u0131k", + "open_1": "A\u00e7 1", + "open_2": "A\u00e7 2", + "open_3": "A\u00e7 3", + "open_4": "A\u00e7\u0131k 4", + "open_all": "Hepsini a\u00e7", + "raise": "Y\u00fckseltmek", + "raise_1": "Y\u00fckselt 1", + "raise_2": "Y\u00fckselt 2", + "raise_3": "Y\u00fckselt 3", + "raise_4": "Y\u00fckselt 4", + "raise_all": "Hepsini Y\u00fckseltin", + "stop": "Durak (favori)", + "stop_1": "Durak 1", + "stop_2": "Durdur 2", + "stop_3": "Durdur 3", + "stop_4": "Durdur 4", + "stop_all": "Hepsini durdur" + }, + "trigger_type": { + "press": "\" {subtype} \" bas\u0131ld\u0131", + "release": "\" {subtype} \" yay\u0131nland\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/uk.json b/homeassistant/components/lutron_caseta/translations/uk.json new file mode 100644 index 00000000000..238e17405ce --- /dev/null +++ b/homeassistant/components/lutron_caseta/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "import_failed": { + "description": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0456\u043c\u043f\u043e\u0440\u0442\u0443\u0432\u0430\u0442\u0438 \u0448\u043b\u044e\u0437 \u0437 \u0444\u0430\u0439\u043b\u0443 'configuration.yaml' (\u0445\u043e\u0441\u0442: {host}).", + "title": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0456\u043c\u043f\u043e\u0440\u0442\u0443\u0432\u0430\u0442\u0438 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e \u0448\u043b\u044e\u0437\u0443." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lutron_caseta/translations/zh-Hant.json b/homeassistant/components/lutron_caseta/translations/zh-Hant.json index 4e8df0d5e9f..50762fafac1 100644 --- a/homeassistant/components/lutron_caseta/translations/zh-Hant.json +++ b/homeassistant/components/lutron_caseta/translations/zh-Hant.json @@ -2,16 +2,75 @@ "config": { "abort": { "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "cannot_connect": "\u9023\u7dda\u5931\u6557" + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "not_lutron_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e Lutron \u88dd\u7f6e" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" }, + "flow_title": "Lutron Cas\u00e9ta {name} ({host})", "step": { "import_failed": { "description": "\u7121\u6cd5\u8a2d\u5b9a\u7531 configuration.yaml \u532f\u5165\u7684 bridge\uff08\u4e3b\u6a5f\uff1a{host}\uff09\u3002", "title": "\u532f\u5165 Cas\u00e9ta bridge \u8a2d\u5b9a\u5931\u6557\u3002" + }, + "link": { + "description": "\u6b32\u8207 {name} ({host}) \u9032\u884c\u914d\u5c0d\uff0c\u65bc\u50b3\u9001\u8868\u683c\u5f8c\u3001\u4e8c\u4e0b Bridge \u5f8c\u65b9\u7684\u9ed1\u8272\u6309\u9215\u3002", + "title": "\u8207 Bridge \u914d\u5c0d" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + }, + "description": "\u8f38\u5165\u88dd\u7f6e IP \u4f4d\u5740\u3002", + "title": "\u81ea\u52d5\u9023\u7dda\u81f3 Bridge" } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "\u7b2c\u4e00\u500b\u6309\u9215", + "button_2": "\u7b2c\u4e8c\u500b\u6309\u9215", + "button_3": "\u7b2c\u4e09\u500b\u6309\u9215", + "button_4": "\u7b2c\u56db\u500b\u6309\u9215", + "close_1": "\u95dc\u9589 1", + "close_2": "\u95dc\u9589 2", + "close_3": "\u95dc\u9589 3", + "close_4": "\u95dc\u9589 4", + "close_all": "\u5168\u90e8\u95dc\u9589", + "group_1_button_1": "\u7b2c\u4e00\u7d44\u7b2c\u4e00\u500b\u6309\u9215", + "group_1_button_2": "\u7b2c\u4e00\u7d44\u7b2c\u4e8c\u500b\u6309\u9215", + "group_2_button_1": "\u7b2c\u4e8c\u7d44\u7b2c\u4e00\u500b\u6309\u9215", + "group_2_button_2": "\u7b2c\u4e8c\u7d44\u7b2c\u4e8c\u500b\u6309\u9215", + "lower": "\u964d\u4f4e ", + "lower_1": "\u964d\u4f4e 1", + "lower_2": "\u964d\u4f4e 2", + "lower_3": "\u964d\u4f4e 3", + "lower_4": "\u964d\u4f4e 4", + "lower_all": "\u5168\u90e8\u964d\u4f4e", + "off": "\u95dc\u9589", + "on": "\u958b\u555f", + "open_1": "\u958b\u555f 1", + "open_2": "\u958b\u555f 2", + "open_3": "\u958b\u555f 3", + "open_4": "\u958b\u555f 4", + "open_all": "\u5168\u90e8\u958b\u555f", + "raise": "\u62ac\u8d77", + "raise_1": "\u62ac\u8d77 1", + "raise_2": "\u62ac\u8d77 2", + "raise_3": "\u62ac\u8d77 3", + "raise_4": "\u62ac\u8d77 4", + "raise_all": "\u5168\u90e8\u62ac\u8d77", + "stop": "\u505c\u6b62\uff08\u6700\u611b\uff09", + "stop_1": "\u505c\u6b62 1", + "stop_2": "\u505c\u6b62 2", + "stop_3": "\u505c\u6b62 3", + "stop_4": "\u505c\u6b62 4", + "stop_all": "\u5168\u90e8\u505c\u6b62" + }, + "trigger_type": { + "press": "\"{subtype}\" \u6309\u4e0b", + "release": "\"{subtype}\" \u91cb\u653e" + } } } \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/ca.json b/homeassistant/components/lyric/translations/ca.json new file mode 100644 index 00000000000..195d3d59262 --- /dev/null +++ b/homeassistant/components/lyric/translations/ca.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", + "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3." + }, + "create_entry": { + "default": "Autenticaci\u00f3 exitosa" + }, + "step": { + "pick_implementation": { + "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/cs.json b/homeassistant/components/lyric/translations/cs.json new file mode 100644 index 00000000000..2a54a82f41b --- /dev/null +++ b/homeassistant/components/lyric/translations/cs.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", + "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace." + }, + "create_entry": { + "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno" + }, + "step": { + "pick_implementation": { + "title": "Vyberte metodu ov\u011b\u0159en\u00ed" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/en.json b/homeassistant/components/lyric/translations/en.json new file mode 100644 index 00000000000..e3849fc17a3 --- /dev/null +++ b/homeassistant/components/lyric/translations/en.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Timeout generating authorize URL.", + "missing_configuration": "The component is not configured. Please follow the documentation." + }, + "create_entry": { + "default": "Successfully authenticated" + }, + "step": { + "pick_implementation": { + "title": "Pick Authentication Method" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/et.json b/homeassistant/components/lyric/translations/et.json new file mode 100644 index 00000000000..c7d46e7e942 --- /dev/null +++ b/homeassistant/components/lyric/translations/et.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Kinnitus-URLi loomise ajal\u00f5pp", + "missing_configuration": "Komponent pole seadistatud. Palun loe dokumentatsiooni." + }, + "create_entry": { + "default": "Tuvastamine \u00f5nnestus" + }, + "step": { + "pick_implementation": { + "title": "Vali tuvastusmeetod" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/it.json b/homeassistant/components/lyric/translations/it.json new file mode 100644 index 00000000000..42536508716 --- /dev/null +++ b/homeassistant/components/lyric/translations/it.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", + "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione." + }, + "create_entry": { + "default": "Autenticazione riuscita" + }, + "step": { + "pick_implementation": { + "title": "Scegli il metodo di autenticazione" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/no.json b/homeassistant/components/lyric/translations/no.json new file mode 100644 index 00000000000..a8f6ce4f9a3 --- /dev/null +++ b/homeassistant/components/lyric/translations/no.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen" + }, + "create_entry": { + "default": "Vellykket godkjenning" + }, + "step": { + "pick_implementation": { + "title": "Velg godkjenningsmetode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/pl.json b/homeassistant/components/lyric/translations/pl.json new file mode 100644 index 00000000000..8c75c11dd7c --- /dev/null +++ b/homeassistant/components/lyric/translations/pl.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", + "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." + }, + "create_entry": { + "default": "Pomy\u015blnie uwierzytelniono" + }, + "step": { + "pick_implementation": { + "title": "Wybierz metod\u0119 uwierzytelniania" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/tr.json b/homeassistant/components/lyric/translations/tr.json new file mode 100644 index 00000000000..773577271d2 --- /dev/null +++ b/homeassistant/components/lyric/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Yetki URL'si olu\u015fturulurken zaman a\u015f\u0131m\u0131 olu\u015ftu.", + "missing_configuration": "Bile\u015fen yap\u0131land\u0131r\u0131lmam\u0131\u015f. L\u00fctfen belgeleri takip edin." + }, + "create_entry": { + "default": "Ba\u015far\u0131yla do\u011fruland\u0131" + }, + "step": { + "pick_implementation": { + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/lyric/translations/zh-Hant.json b/homeassistant/components/lyric/translations/zh-Hant.json new file mode 100644 index 00000000000..b740fd3e063 --- /dev/null +++ b/homeassistant/components/lyric/translations/zh-Hant.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", + "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" + }, + "create_entry": { + "default": "\u5df2\u6210\u529f\u8a8d\u8b49" + }, + "step": { + "pick_implementation": { + "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/translations/de.json b/homeassistant/components/mailgun/translations/de.json index f684f822fd5..118192b6516 100644 --- a/homeassistant/components/mailgun/translations/de.json +++ b/homeassistant/components/mailgun/translations/de.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", + "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." + }, "create_entry": { - "default": "Um Ereignisse an den Home Assistant zu senden, musst [Webhooks mit Mailgun]({mailgun_url}) einrichten. \n\n F\u00fclle die folgenden Informationen aus: \n\n - URL: `{webhook_url}` \n - Methode: POST \n - Inhaltstyp: application/json \n\nLies in der [Dokumentation]({docs_url}) wie du Automationen f\u00fcr die Verarbeitung eingehender Daten konfigurierst." + "default": "Um Ereignisse an Home Assistant zu senden, musst du [Webhooks mit Mailgun]({mailgun_url}) einrichten. \n\n F\u00fclle die folgenden Informationen aus: \n\n - URL: `{webhook_url}` \n - Methode: POST \n - Inhaltstyp: application/json \n\nLies in der [Dokumentation]({docs_url}), wie du Automationen f\u00fcr die Verarbeitung eingehender Daten konfigurierst." }, "step": { "user": { diff --git a/homeassistant/components/mailgun/translations/tr.json b/homeassistant/components/mailgun/translations/tr.json new file mode 100644 index 00000000000..84adcdf8225 --- /dev/null +++ b/homeassistant/components/mailgun/translations/tr.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mailgun/translations/uk.json b/homeassistant/components/mailgun/translations/uk.json new file mode 100644 index 00000000000..d999b52085a --- /dev/null +++ b/homeassistant/components/mailgun/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "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.", + "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c." + }, + "create_entry": { + "default": "\u0414\u043b\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043a\u0438 \u043f\u043e\u0434\u0456\u0439 \u0432 Home Assistant \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Webhook \u0434\u043b\u044f [Mailgun]({mailgun_url}). \n\n\u0417\u0430\u043f\u043e\u0432\u043d\u0456\u0442\u044c \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e: \n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/json \n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0456\u0439 \u043f\u043e \u043e\u0431\u0440\u043e\u0431\u0446\u0456 \u0434\u0430\u043d\u0438\u0445, \u0449\u043e \u043d\u0430\u0434\u0445\u043e\u0434\u044f\u0442\u044c." + }, + "step": { + "user": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Mailgun?", + "title": "Mailgun" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/maxcube/binary_sensor.py b/homeassistant/components/maxcube/binary_sensor.py index 06b53456973..376076352a6 100644 --- a/homeassistant/components/maxcube/binary_sensor.py +++ b/homeassistant/components/maxcube/binary_sensor.py @@ -16,7 +16,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): name = f"{cube.room_by_id(device.room_id).name} {device.name}" # Only add Window Shutters - if cube.is_windowshutter(device): + if device.is_windowshutter(): devices.append(MaxCubeShutter(handler, name, device.rf_address)) if devices: diff --git a/homeassistant/components/maxcube/climate.py b/homeassistant/components/maxcube/climate.py index e222784ca57..c17cc988c1d 100644 --- a/homeassistant/components/maxcube/climate.py +++ b/homeassistant/components/maxcube/climate.py @@ -70,7 +70,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for device in cube.devices: name = f"{cube.room_by_id(device.room_id).name} {device.name}" - if cube.is_thermostat(device) or cube.is_wallthermostat(device): + if device.is_thermostat() or device.is_wallthermostat(): devices.append(MaxCubeClimate(handler, name, device.rf_address)) if devices: @@ -180,11 +180,11 @@ class MaxCubeClimate(ClimateEntity): device = cube.device_by_rf(self._rf_address) valve = 0 - if cube.is_thermostat(device): + if device.is_thermostat(): valve = device.valve_position - elif cube.is_wallthermostat(device): + elif device.is_wallthermostat(): for device in cube.devices_by_room(cube.room_by_id(device.room_id)): - if cube.is_thermostat(device) and device.valve_position > 0: + if device.is_thermostat() and device.valve_position > 0: valve = device.valve_position break else: @@ -287,7 +287,7 @@ class MaxCubeClimate(ClimateEntity): cube = self._cubehandle.cube device = cube.device_by_rf(self._rf_address) - if not cube.is_thermostat(device): + if not device.is_thermostat(): return {} return {ATTR_VALVE_POSITION: device.valve_position} diff --git a/homeassistant/components/maxcube/manifest.json b/homeassistant/components/maxcube/manifest.json index 0aae92c2079..e6badb254f7 100644 --- a/homeassistant/components/maxcube/manifest.json +++ b/homeassistant/components/maxcube/manifest.json @@ -2,6 +2,6 @@ "domain": "maxcube", "name": "eQ-3 MAX!", "documentation": "https://www.home-assistant.io/integrations/maxcube", - "requirements": ["maxcube-api==0.1.0"], + "requirements": ["maxcube-api==0.3.0"], "codeowners": [] } diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 2c17d85f7b3..5a09171df80 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -2,7 +2,7 @@ "domain": "media_extractor", "name": "Media Extractor", "documentation": "https://www.home-assistant.io/integrations/media_extractor", - "requirements": ["youtube_dl==2020.12.29"], + "requirements": ["youtube_dl==2021.01.16"], "dependencies": ["media_player"], "codeowners": [], "quality_scale": "internal" diff --git a/homeassistant/components/media_player/translations/tr.json b/homeassistant/components/media_player/translations/tr.json index 0130b5fb94c..1f46c6a8bc7 100644 --- a/homeassistant/components/media_player/translations/tr.json +++ b/homeassistant/components/media_player/translations/tr.json @@ -1,4 +1,10 @@ { + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} bo\u015fta", + "is_off": "{entity_name} kapal\u0131" + } + }, "state": { "_": { "idle": "Bo\u015fta", diff --git a/homeassistant/components/media_player/translations/uk.json b/homeassistant/components/media_player/translations/uk.json index f475829a524..21c7f2897a3 100644 --- a/homeassistant/components/media_player/translations/uk.json +++ b/homeassistant/components/media_player/translations/uk.json @@ -1,7 +1,16 @@ { + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} \u0432 \u0440\u0435\u0436\u0438\u043c\u0456 \u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f", + "is_off": "{entity_name} \u0443 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_on": "{entity_name} \u0443 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_paused": "{entity_name} \u043d\u0430 \u043f\u0430\u0443\u0437\u0456", + "is_playing": "{entity_name} \u0432\u0456\u0434\u0442\u0432\u043e\u0440\u044e\u0454 \u043c\u0435\u0434\u0456\u0430" + } + }, "state": { "_": { - "idle": "\u0411\u0435\u0437\u0434\u0456\u044f\u043b\u044c\u043d\u0456\u0441\u0442\u044c", + "idle": "\u041e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f", "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e", "on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e", "paused": "\u041f\u0440\u0438\u0437\u0443\u043f\u0438\u043d\u0435\u043d\u043e", @@ -9,5 +18,5 @@ "standby": "\u041e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f" } }, - "title": "\u041c\u0435\u0434\u0456\u0430 \u043f\u043b\u0435\u0454\u0440" + "title": "\u041c\u0435\u0434\u0456\u0430\u043f\u0440\u043e\u0433\u0440\u0430\u0432\u0430\u0447" } \ No newline at end of file diff --git a/homeassistant/components/media_source/local_source.py b/homeassistant/components/media_source/local_source.py index 6c60da562e0..d7a2bdfd938 100644 --- a/homeassistant/components/media_source/local_source.py +++ b/homeassistant/components/media_source/local_source.py @@ -10,7 +10,7 @@ from homeassistant.components.media_player.const import MEDIA_CLASS_DIRECTORY from homeassistant.components.media_player.errors import BrowseError from homeassistant.components.media_source.error import Unresolvable from homeassistant.core import HomeAssistant, callback -from homeassistant.util import sanitize_path +from homeassistant.util import raise_if_invalid_filename from .const import DOMAIN, MEDIA_CLASS_MAP, MEDIA_MIME_TYPES from .models import BrowseMediaSource, MediaSource, MediaSourceItem, PlayMedia @@ -50,8 +50,10 @@ class LocalSource(MediaSource): if source_dir_id not in self.hass.config.media_dirs: raise Unresolvable("Unknown source directory.") - if location != sanitize_path(location): - raise Unresolvable("Invalid path.") + try: + raise_if_invalid_filename(location) + except ValueError as err: + raise Unresolvable("Invalid path.") from err return source_dir_id, location @@ -189,8 +191,10 @@ class LocalMediaView(HomeAssistantView): self, request: web.Request, source_dir_id: str, location: str ) -> web.FileResponse: """Start a GET request.""" - if location != sanitize_path(location): - raise web.HTTPNotFound() + try: + raise_if_invalid_filename(location) + except ValueError as err: + raise web.HTTPBadRequest() from err if source_dir_id not in self.hass.config.media_dirs: raise web.HTTPNotFound() diff --git a/homeassistant/components/melcloud/translations/de.json b/homeassistant/components/melcloud/translations/de.json index 640c96e47c4..54ae78f8680 100644 --- a/homeassistant/components/melcloud/translations/de.json +++ b/homeassistant/components/melcloud/translations/de.json @@ -4,7 +4,7 @@ "already_configured": "Die MELCloud-Integration ist bereits f\u00fcr diese E-Mail konfiguriert. Das Zugriffstoken wurde aktualisiert." }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen. Bitte versuchen Sie es erneut.", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/melcloud/translations/tr.json b/homeassistant/components/melcloud/translations/tr.json new file mode 100644 index 00000000000..6bce50f3de6 --- /dev/null +++ b/homeassistant/components/melcloud/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "MELCloud entegrasyonu bu e-posta i\u00e7in zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Eri\u015fim belirteci yenilendi." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "E-posta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/translations/uk.json b/homeassistant/components/melcloud/translations/uk.json new file mode 100644 index 00000000000..001239a8b47 --- /dev/null +++ b/homeassistant/components/melcloud/translations/uk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f MELCloud \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0430 \u0434\u043b\u044f \u0446\u0456\u0454\u0457 \u0430\u0434\u0440\u0435\u0441\u0438 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438. \u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0439." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438" + }, + "description": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0456\u0442\u044c\u0441\u044f, \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u044e\u0447\u0438 \u0441\u0432\u0456\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 MELCloud.", + "title": "MELCloud" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met/translations/de.json b/homeassistant/components/met/translations/de.json index 901b4fb97b5..e2bb171c749 100644 --- a/homeassistant/components/met/translations/de.json +++ b/homeassistant/components/met/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "error": { + "already_configured": "Der Dienst ist bereits konfiguriert" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/met/translations/tr.json b/homeassistant/components/met/translations/tr.json new file mode 100644 index 00000000000..d256711728c --- /dev/null +++ b/homeassistant/components/met/translations/tr.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "user": { + "data": { + "latitude": "Enlem", + "longitude": "Boylam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/met/translations/uk.json b/homeassistant/components/met/translations/uk.json new file mode 100644 index 00000000000..d980db91147 --- /dev/null +++ b/homeassistant/components/met/translations/uk.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "step": { + "user": { + "data": { + "elevation": "\u0412\u0438\u0441\u043e\u0442\u0430", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "description": "\u041d\u043e\u0440\u0432\u0435\u0437\u044c\u043a\u0438\u0439 \u043c\u0435\u0442\u0435\u043e\u0440\u043e\u043b\u043e\u0433\u0456\u0447\u043d\u0438\u0439 \u0456\u043d\u0441\u0442\u0438\u0442\u0443\u0442.", + "title": "\u041c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py index d642d3c6e0f..a2e9eeb2799 100644 --- a/homeassistant/components/meteo_france/const.py +++ b/homeassistant/components/meteo_france/const.py @@ -36,6 +36,8 @@ COORDINATOR_RAIN = "coordinator_rain" COORDINATOR_ALERT = "coordinator_alert" UNDO_UPDATE_LISTENER = "undo_update_listener" ATTRIBUTION = "Data provided by Météo-France" +MODEL = "Météo-France mobile API" +MANUFACTURER = "Météo-France" CONF_CITY = "city" FORECAST_MODE_HOURLY = "hourly" diff --git a/homeassistant/components/meteo_france/sensor.py b/homeassistant/components/meteo_france/sensor.py index 8e6b036202f..201cca7ae9d 100644 --- a/homeassistant/components/meteo_france/sensor.py +++ b/homeassistant/components/meteo_france/sensor.py @@ -29,6 +29,8 @@ from .const import ( ENTITY_ICON, ENTITY_NAME, ENTITY_UNIT, + MANUFACTURER, + MODEL, SENSOR_TYPES, ) @@ -94,6 +96,17 @@ class MeteoFranceSensor(CoordinatorEntity): """Return the name.""" return self._name + @property + def device_info(self): + """Return the device info.""" + return { + "identifiers": {(DOMAIN, self.platform.config_entry.unique_id)}, + "name": self.coordinator.name, + "manufacturer": MANUFACTURER, + "model": MODEL, + "entry_type": "service", + } + @property def state(self): """Return the state.""" diff --git a/homeassistant/components/meteo_france/translations/de.json b/homeassistant/components/meteo_france/translations/de.json index 65313f16c41..74637594d5f 100644 --- a/homeassistant/components/meteo_france/translations/de.json +++ b/homeassistant/components/meteo_france/translations/de.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "Stadt bereits konfiguriert", - "unknown": "Unbekannter Fehler: Bitte versuchen Sie es sp\u00e4ter erneut" + "already_configured": "Standort ist bereits konfiguriert", + "unknown": "Unerwarteter Fehler" }, "error": { "empty": "Kein Ergebnis bei der Stadtsuche: Bitte \u00fcberpr\u00fcfe das Stadtfeld" diff --git a/homeassistant/components/meteo_france/translations/tr.json b/homeassistant/components/meteo_france/translations/tr.json index 57fc9f76881..59c3886a900 100644 --- a/homeassistant/components/meteo_france/translations/tr.json +++ b/homeassistant/components/meteo_france/translations/tr.json @@ -1,7 +1,28 @@ { "config": { + "abort": { + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "unknown": "Beklenmeyen hata" + }, "error": { "empty": "\u015eehir aramas\u0131nda sonu\u00e7 yok: l\u00fctfen \u015fehir alan\u0131n\u0131 kontrol edin" + }, + "step": { + "user": { + "data": { + "city": "\u015eehir" + }, + "title": "M\u00e9t\u00e9o-Fransa" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "mode": "Tahmin modu" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/meteo_france/translations/uk.json b/homeassistant/components/meteo_france/translations/uk.json new file mode 100644 index 00000000000..a84c230e218 --- /dev/null +++ b/homeassistant/components/meteo_france/translations/uk.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "empty": "\u041d\u0435\u043c\u0430\u0454 \u0440\u0435\u0437\u0443\u043b\u044c\u0442\u0430\u0442\u0456\u0432 \u043f\u043e\u0448\u0443\u043a\u0443. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043f\u043e\u043b\u0435 \"\u041c\u0456\u0441\u0442\u043e\"." + }, + "step": { + "cities": { + "data": { + "city": "\u041c\u0456\u0441\u0442\u043e" + }, + "description": "\u041e\u0431\u0435\u0440\u0456\u0442\u044c \u043c\u0456\u0441\u0442\u043e \u0437\u0456 \u0441\u043f\u0438\u0441\u043a\u0443", + "title": "M\u00e9t\u00e9o-France" + }, + "user": { + "data": { + "city": "\u041c\u0456\u0441\u0442\u043e" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u043e\u0448\u0442\u043e\u0432\u0438\u0439 \u0456\u043d\u0434\u0435\u043a\u0441 (\u0440\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0454\u0442\u044c\u0441\u044f \u0442\u0456\u043b\u044c\u043a\u0438 \u0434\u043b\u044f \u0424\u0440\u0430\u043d\u0446\u0456\u0457) \u0430\u0431\u043e \u043d\u0430\u0437\u0432\u0443 \u043c\u0456\u0441\u0442\u0430", + "title": "M\u00e9t\u00e9o-France" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "mode": "\u0420\u0435\u0436\u0438\u043c \u043f\u0440\u043e\u0433\u043d\u043e\u0437\u0443" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/meteo_france/weather.py b/homeassistant/components/meteo_france/weather.py index ffb468574b8..09e062cc715 100644 --- a/homeassistant/components/meteo_france/weather.py +++ b/homeassistant/components/meteo_france/weather.py @@ -28,6 +28,8 @@ from .const import ( DOMAIN, FORECAST_MODE_DAILY, FORECAST_MODE_HOURLY, + MANUFACTURER, + MODEL, ) _LOGGER = logging.getLogger(__name__) @@ -83,6 +85,17 @@ class MeteoFranceWeather(CoordinatorEntity, WeatherEntity): """Return the name of the sensor.""" return self._city_name + @property + def device_info(self): + """Return the device info.""" + return { + "identifiers": {(DOMAIN, self.platform.config_entry.unique_id)}, + "name": self.coordinator.name, + "manufacturer": MANUFACTURER, + "model": MODEL, + "entry_type": "service", + } + @property def condition(self): """Return the current condition.""" diff --git a/homeassistant/components/metoffice/translations/de.json b/homeassistant/components/metoffice/translations/de.json index 74c204b9683..7b92af96c99 100644 --- a/homeassistant/components/metoffice/translations/de.json +++ b/homeassistant/components/metoffice/translations/de.json @@ -4,6 +4,7 @@ "already_configured": "Service ist bereits konfiguriert" }, "error": { + "cannot_connect": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/metoffice/translations/tr.json b/homeassistant/components/metoffice/translations/tr.json new file mode 100644 index 00000000000..55064a139ef --- /dev/null +++ b/homeassistant/components/metoffice/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "latitude": "Enlem", + "longitude": "Boylam" + }, + "description": "Enlem ve boylam, en yak\u0131n hava istasyonunu bulmak i\u00e7in kullan\u0131lacakt\u0131r." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/metoffice/translations/uk.json b/homeassistant/components/metoffice/translations/uk.json new file mode 100644 index 00000000000..53ab2115e82 --- /dev/null +++ b/homeassistant/components/metoffice/translations/uk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430" + }, + "description": "\u0428\u0438\u0440\u043e\u0442\u0430 \u0456 \u0434\u043e\u0432\u0433\u043e\u0442\u0430 \u0431\u0443\u0434\u0443\u0442\u044c \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u0456 \u0434\u043b\u044f \u043f\u043e\u0448\u0443\u043a\u0443 \u043d\u0430\u0439\u0431\u043b\u0438\u0436\u0447\u043e\u0457 \u043c\u0435\u0442\u0435\u043e\u0441\u0442\u0430\u043d\u0446\u0456\u0457.", + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e Met Office UK" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/device_tracker.py b/homeassistant/components/mikrotik/device_tracker.py index 01075a5f2d8..b9e0b051aba 100644 --- a/homeassistant/components/mikrotik/device_tracker.py +++ b/homeassistant/components/mikrotik/device_tracker.py @@ -16,6 +16,10 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) +# These are normalized to ATTR_IP and ATTR_MAC to conform +# to device_tracker +FILTER_ATTRS = ("ip_address", "mac_address") + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up device tracker for Mikrotik component.""" @@ -93,6 +97,21 @@ class MikrotikHubTracker(ScannerEntity): """Return the name of the client.""" return self.device.name + @property + def hostname(self) -> str: + """Return the hostname of the client.""" + return self.device.name + + @property + def mac_address(self) -> str: + """Return the mac address of the client.""" + return self.device.mac + + @property + def ip_address(self) -> str: + """Return the mac address of the client.""" + return self.device.ip_address + @property def unique_id(self) -> str: """Return a unique identifier for this device.""" @@ -107,7 +126,7 @@ class MikrotikHubTracker(ScannerEntity): def device_state_attributes(self): """Return the device state attributes.""" if self.is_connected: - return self.device.attrs + return {k: v for k, v in self.device.attrs.items() if k not in FILTER_ATTRS} return None @property diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py index 4136377fde9..28a78d0ee1a 100644 --- a/homeassistant/components/mikrotik/hub.py +++ b/homeassistant/components/mikrotik/hub.py @@ -54,6 +54,11 @@ class Device: """Return device name.""" return self._params.get("host-name", self.mac) + @property + def ip_address(self): + """Return device primary ip address.""" + return self._params.get("address") + @property def mac(self): """Return device mac.""" diff --git a/homeassistant/components/mikrotik/translations/de.json b/homeassistant/components/mikrotik/translations/de.json index 4211077c82c..82ea47dc4bf 100644 --- a/homeassistant/components/mikrotik/translations/de.json +++ b/homeassistant/components/mikrotik/translations/de.json @@ -1,10 +1,11 @@ { "config": { "abort": { - "already_configured": "Mikrotik ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "name_exists": "Name vorhanden" }, "step": { @@ -25,7 +26,7 @@ "step": { "device_tracker": { "data": { - "arp_ping": "ARP Ping aktivieren", + "arp_ping": "ARP-Ping aktivieren", "force_dhcp": "Erzwingen Sie das Scannen \u00fcber DHCP" } } diff --git a/homeassistant/components/mikrotik/translations/tr.json b/homeassistant/components/mikrotik/translations/tr.json new file mode 100644 index 00000000000..cffcc65151c --- /dev/null +++ b/homeassistant/components/mikrotik/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/uk.json b/homeassistant/components/mikrotik/translations/uk.json new file mode 100644 index 00000000000..b44d5979d13 --- /dev/null +++ b/homeassistant/components/mikrotik/translations/uk.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "name_exists": "\u0426\u044f \u043d\u0430\u0437\u0432\u0430 \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", + "verify_ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 SSL" + }, + "title": "MikroTik" + } + } + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 ARP-\u043f\u0456\u043d\u0433", + "detection_time": "\u0427\u0430\u0441 \u0432\u0456\u0434 \u043e\u0441\u0442\u0430\u043d\u043d\u044c\u043e\u0433\u043e \u0441\u0435\u0430\u043d\u0441\u0443 \u0437\u0432'\u044f\u0437\u043a\u0443 \u0437 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c (\u0441\u0435\u043a.), \u043f\u043e \u0437\u0430\u043a\u0456\u043d\u0447\u0435\u043d\u043d\u044e \u044f\u043a\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043e\u0442\u0440\u0438\u043c\u0430\u0454 \u0441\u0442\u0430\u0442\u0443\u0441 \"\u041d\u0435 \u0432\u0434\u043e\u043c\u0430\".", + "force_dhcp": "\u041f\u0440\u0438\u043c\u0443\u0441\u043e\u0432\u0435 \u0441\u043a\u0430\u043d\u0443\u0432\u0430\u043d\u043d\u044f \u0437 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f\u043c DHCP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mill/translations/de.json b/homeassistant/components/mill/translations/de.json index 886e7e3c458..63b6b7ea6e9 100644 --- a/homeassistant/components/mill/translations/de.json +++ b/homeassistant/components/mill/translations/de.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Account ist bereits konfiguriert" }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/mill/translations/fr.json b/homeassistant/components/mill/translations/fr.json index e171086a084..ffcff15ade8 100644 --- a/homeassistant/components/mill/translations/fr.json +++ b/homeassistant/components/mill/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9" + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" }, "error": { "cannot_connect": "\u00c9chec de connexion" diff --git a/homeassistant/components/mill/translations/tr.json b/homeassistant/components/mill/translations/tr.json new file mode 100644 index 00000000000..0f14728873a --- /dev/null +++ b/homeassistant/components/mill/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mill/translations/uk.json b/homeassistant/components/mill/translations/uk.json new file mode 100644 index 00000000000..b8a5aea578e --- /dev/null +++ b/homeassistant/components/mill/translations/uk.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/translations/de.json b/homeassistant/components/minecraft_server/translations/de.json index 484be7bd418..a0bbe60a842 100644 --- a/homeassistant/components/minecraft_server/translations/de.json +++ b/homeassistant/components/minecraft_server/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Der Host ist bereits konfiguriert." + "already_configured": "Der Dienst ist bereits konfiguriert" }, "error": { "cannot_connect": "Verbindung zum Server fehlgeschlagen. Bitte \u00fcberpr\u00fcfe den Host und den Port und versuche es erneut. Stelle au\u00dferdem sicher, dass Du mindestens Minecraft Version 1.7 auf Deinem Server ausf\u00fchrst.", diff --git a/homeassistant/components/minecraft_server/translations/tr.json b/homeassistant/components/minecraft_server/translations/tr.json index 7527294a3c7..422dab32a01 100644 --- a/homeassistant/components/minecraft_server/translations/tr.json +++ b/homeassistant/components/minecraft_server/translations/tr.json @@ -11,7 +11,7 @@ "step": { "user": { "data": { - "host": "Host", + "host": "Ana Bilgisayar", "name": "Ad" }, "description": "G\u00f6zetmeye izin vermek i\u00e7in Minecraft server nesnesini ayarla.", diff --git a/homeassistant/components/minecraft_server/translations/uk.json b/homeassistant/components/minecraft_server/translations/uk.json new file mode 100644 index 00000000000..0c8528b2cab --- /dev/null +++ b/homeassistant/components/minecraft_server/translations/uk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u0427\u0438 \u043d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f \u0434\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0456\u0441\u0442\u044c \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0445 \u0434\u0430\u043d\u0438\u0445 \u0456 \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0437\u043d\u043e\u0432\u0443. \u0422\u0430\u043a\u043e\u0436 \u043f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u043d\u0430 \u0412\u0430\u0448\u043e\u043c\u0443 \u0441\u0435\u0440\u0432\u0435\u0440\u0456 \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0439 Minecraft \u0432\u0435\u0440\u0441\u0456\u0457 1.7, \u0430\u0431\u043e \u0432\u0438\u0449\u0435.", + "invalid_ip": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0430 IP-\u0430\u0434\u0440\u0435\u0441\u0430 (\u043d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0432\u0438\u0437\u043d\u0430\u0447\u0438\u0442\u0438 MAC-\u0430\u0434\u0440\u0435\u0441\u0443).", + "invalid_port": "\u041f\u043e\u0440\u0442 \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0432 \u0434\u0456\u0430\u043f\u0430\u0437\u043e\u043d\u0456 \u0432\u0456\u0434 1024 \u0434\u043e 65535." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u0446\u0435\u0439 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0434\u043b\u044f \u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433\u0443 \u0412\u0430\u0448\u043e\u0433\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Minecraft.", + "title": "Minecraft Server" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mobile_app/notify.py b/homeassistant/components/mobile_app/notify.py index b3482a70fb9..46a34fa7a85 100644 --- a/homeassistant/components/mobile_app/notify.py +++ b/homeassistant/components/mobile_app/notify.py @@ -2,6 +2,7 @@ import asyncio import logging +import aiohttp import async_timeout from homeassistant.components.notify import ( @@ -168,3 +169,5 @@ class MobileAppNotificationService(BaseNotificationService): except asyncio.TimeoutError: _LOGGER.error("Timeout sending notification to %s", push_url) + except aiohttp.ClientError as err: + _LOGGER.error("Error sending notification to %s: %r", push_url, err) diff --git a/homeassistant/components/mobile_app/translations/tr.json b/homeassistant/components/mobile_app/translations/tr.json new file mode 100644 index 00000000000..10d79751ec1 --- /dev/null +++ b/homeassistant/components/mobile_app/translations/tr.json @@ -0,0 +1,7 @@ +{ + "device_automation": { + "action_type": { + "notify": "Bildirim g\u00f6nder" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mobile_app/translations/uk.json b/homeassistant/components/mobile_app/translations/uk.json index 4a48dd3775d..db471bbdc7f 100644 --- a/homeassistant/components/mobile_app/translations/uk.json +++ b/homeassistant/components/mobile_app/translations/uk.json @@ -1,9 +1,17 @@ { "config": { + "abort": { + "install_app": "\u0412\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 \u043c\u043e\u0431\u0456\u043b\u044c\u043d\u0438\u0439 \u0434\u043e\u0434\u0430\u0442\u043e\u043a, \u0449\u043e\u0431 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e \u0437 Home Assistant. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({apps_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0441\u043f\u0438\u0441\u043a\u0443 \u0441\u0443\u043c\u0456\u0441\u043d\u0438\u0445 \u0434\u043e\u0434\u0430\u0442\u043a\u0456\u0432." + }, "step": { "confirm": { - "description": "\u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u043c\u043e\u0431\u0456\u043b\u044c\u043d\u043e\u0433\u043e \u0434\u043e\u0434\u0430\u0442\u043a\u0430?" + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043c\u043e\u0431\u0456\u043b\u044c\u043d\u0438\u0439 \u0434\u043e\u0434\u0430\u0442\u043e\u043a?" } } + }, + "device_automation": { + "action_type": { + "notify": "\u041d\u0430\u0434\u0456\u0441\u043b\u0430\u0442\u0438 \u0441\u043f\u043e\u0432\u0456\u0449\u0435\u043d\u043d\u044f" + } } } \ No newline at end of file diff --git a/homeassistant/components/mochad/light.py b/homeassistant/components/mochad/light.py index 90af33bd0cd..a3a06a58ea5 100644 --- a/homeassistant/components/mochad/light.py +++ b/homeassistant/components/mochad/light.py @@ -1,5 +1,8 @@ """Support for X10 dimmer over Mochad.""" +import logging + from pymochad import device +from pymochad.exceptions import MochadException import voluptuous as vol from homeassistant.components.light import ( @@ -13,6 +16,7 @@ from homeassistant.helpers import config_validation as cv from . import CONF_COMM_TYPE, DOMAIN, REQ_LOCK +_LOGGER = logging.getLogger(__name__) CONF_BRIGHTNESS_LEVELS = "brightness_levels" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -103,30 +107,42 @@ class MochadLight(LightEntity): def turn_on(self, **kwargs): """Send the command to turn the light on.""" + _LOGGER.debug("Reconnect %s:%s", self._controller.server, self._controller.port) brightness = kwargs.get(ATTR_BRIGHTNESS, 255) with REQ_LOCK: - if self._brightness_levels > 32: - out_brightness = self._calculate_brightness_value(brightness) - self.light.send_cmd(f"xdim {out_brightness}") - self._controller.read_data() - else: - self.light.send_cmd("on") - self._controller.read_data() - # There is no persistence for X10 modules so a fresh on command - # will be full brightness - if self._brightness == 0: - self._brightness = 255 - self._adjust_brightness(brightness) - self._brightness = brightness - self._state = True + try: + # Recycle socket on new command to recover mochad connection + self._controller.reconnect() + if self._brightness_levels > 32: + out_brightness = self._calculate_brightness_value(brightness) + self.light.send_cmd(f"xdim {out_brightness}") + self._controller.read_data() + else: + self.light.send_cmd("on") + self._controller.read_data() + # There is no persistence for X10 modules so a fresh on command + # will be full brightness + if self._brightness == 0: + self._brightness = 255 + self._adjust_brightness(brightness) + self._brightness = brightness + self._state = True + except (MochadException, OSError) as exc: + _LOGGER.error("Error with mochad communication: %s", exc) def turn_off(self, **kwargs): """Send the command to turn the light on.""" + _LOGGER.debug("Reconnect %s:%s", self._controller.server, self._controller.port) with REQ_LOCK: - self.light.send_cmd("off") - self._controller.read_data() - # There is no persistence for X10 modules so we need to prepare - # to track a fresh on command will full brightness - if self._brightness_levels == 31: - self._brightness = 0 - self._state = False + try: + # Recycle socket on new command to recover mochad connection + self._controller.reconnect() + self.light.send_cmd("off") + self._controller.read_data() + # There is no persistence for X10 modules so we need to prepare + # to track a fresh on command will full brightness + if self._brightness_levels == 31: + self._brightness = 0 + self._state = False + except (MochadException, OSError) as exc: + _LOGGER.error("Error with mochad communication: %s", exc) diff --git a/homeassistant/components/mold_indicator/sensor.py b/homeassistant/components/mold_indicator/sensor.py index 81ae571ed79..e2d9909c7ca 100644 --- a/homeassistant/components/mold_indicator/sensor.py +++ b/homeassistant/components/mold_indicator/sensor.py @@ -378,7 +378,10 @@ class MoldIndicator(Entity): def device_state_attributes(self): """Return the state attributes.""" if self._is_metric: - return {ATTR_DEWPOINT: self._dewpoint, ATTR_CRITICAL_TEMP: self._crit_temp} + return { + ATTR_DEWPOINT: round(self._dewpoint, 2), + ATTR_CRITICAL_TEMP: round(self._crit_temp, 2), + } dewpoint = ( util.temperature.celsius_to_fahrenheit(self._dewpoint) @@ -392,4 +395,7 @@ class MoldIndicator(Entity): else None ) - return {ATTR_DEWPOINT: dewpoint, ATTR_CRITICAL_TEMP: crit_temp} + return { + ATTR_DEWPOINT: round(dewpoint, 2), + ATTR_CRITICAL_TEMP: round(crit_temp, 2), + } diff --git a/homeassistant/components/monoprice/translations/de.json b/homeassistant/components/monoprice/translations/de.json index 820d3a972d3..8f6d1d88196 100644 --- a/homeassistant/components/monoprice/translations/de.json +++ b/homeassistant/components/monoprice/translations/de.json @@ -4,7 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/monoprice/translations/tr.json b/homeassistant/components/monoprice/translations/tr.json new file mode 100644 index 00000000000..7c622a3cb4a --- /dev/null +++ b/homeassistant/components/monoprice/translations/tr.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "port": "Port", + "source_1": "Kaynak #1 ad\u0131", + "source_2": "Kaynak #2 ad\u0131", + "source_3": "Kaynak #3 ad\u0131", + "source_4": "Kaynak #4 ad\u0131", + "source_5": "Kaynak #5 ad\u0131", + "source_6": "Kaynak #6 ad\u0131" + }, + "title": "Cihaza ba\u011flan\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/monoprice/translations/uk.json b/homeassistant/components/monoprice/translations/uk.json new file mode 100644 index 00000000000..08857cc26f9 --- /dev/null +++ b/homeassistant/components/monoprice/translations/uk.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442", + "source_1": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #1", + "source_2": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #2", + "source_3": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #3", + "source_4": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #4", + "source_5": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #5", + "source_6": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #6" + }, + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #1", + "source_2": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #2", + "source_3": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #3", + "source_4": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #4", + "source_5": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #5", + "source_6": "\u041d\u0430\u0437\u0432\u0430 \u0434\u0436\u0435\u0440\u0435\u043b\u0430 #6" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u0436\u0435\u0440\u0435\u043b" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/moon/translations/sensor.uk.json b/homeassistant/components/moon/translations/sensor.uk.json index 71c2d80eb98..f916c03c3a1 100644 --- a/homeassistant/components/moon/translations/sensor.uk.json +++ b/homeassistant/components/moon/translations/sensor.uk.json @@ -4,7 +4,11 @@ "first_quarter": "\u041f\u0435\u0440\u0448\u0430 \u0447\u0432\u0435\u0440\u0442\u044c", "full_moon": "\u041f\u043e\u0432\u043d\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c", "last_quarter": "\u041e\u0441\u0442\u0430\u043d\u043d\u044f \u0447\u0432\u0435\u0440\u0442\u044c", - "new_moon": "\u041d\u043e\u0432\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c" + "new_moon": "\u041d\u043e\u0432\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c", + "waning_crescent": "\u0421\u0442\u0430\u0440\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c", + "waning_gibbous": "\u0421\u043f\u0430\u0434\u0430\u044e\u0447\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c", + "waxing_crescent": "\u041c\u043e\u043b\u043e\u0434\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c", + "waxing_gibbous": "\u041f\u0440\u0438\u0431\u0443\u0432\u0430\u044e\u0447\u0438\u0439 \u043c\u0456\u0441\u044f\u0446\u044c" } } } \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/manifest.json b/homeassistant/components/motion_blinds/manifest.json index ce781266a6e..ec2823dbd2e 100644 --- a/homeassistant/components/motion_blinds/manifest.json +++ b/homeassistant/components/motion_blinds/manifest.json @@ -3,6 +3,6 @@ "name": "Motion Blinds", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/motion_blinds", - "requirements": ["motionblinds==0.4.7"], + "requirements": ["motionblinds==0.4.8"], "codeowners": ["@starkillerOG"] } diff --git a/homeassistant/components/motion_blinds/translations/ca.json b/homeassistant/components/motion_blinds/translations/ca.json index a4bf96457e6..b83746b9ccf 100644 --- a/homeassistant/components/motion_blinds/translations/ca.json +++ b/homeassistant/components/motion_blinds/translations/ca.json @@ -5,14 +5,31 @@ "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", "connection_error": "Ha fallat la connexi\u00f3" }, + "error": { + "discovery_error": "No s'ha pogut descobrir cap Motion Gateway" + }, "flow_title": "Motion Blinds", "step": { + "connect": { + "data": { + "api_key": "Clau API" + }, + "description": "Necessitar\u00e0s la clau API de 16 car\u00e0cters, consulta les instruccions a https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key.", + "title": "Motion Blinds" + }, + "select": { + "data": { + "select_ip": "Adre\u00e7a IP" + }, + "description": "Torna a executar la configuraci\u00f3 si vols connectar m\u00e9s Motion Gateways", + "title": "Selecciona el Motion Gateway que vulguis connectar" + }, "user": { "data": { "api_key": "Clau API", "host": "Adre\u00e7a IP" }, - "description": "Necessitar\u00e0s el token d'API de 16 car\u00e0cters, consulta les instruccions a https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key.", + "description": "Connecta el teu Motion Gateway, si no es configura l'adre\u00e7a IP, s'utilitza el descobriment autom\u00e0tic", "title": "Motion Blinds" } } diff --git a/homeassistant/components/motion_blinds/translations/cs.json b/homeassistant/components/motion_blinds/translations/cs.json index 41b5db3c83e..899f04d7cd4 100644 --- a/homeassistant/components/motion_blinds/translations/cs.json +++ b/homeassistant/components/motion_blinds/translations/cs.json @@ -7,12 +7,21 @@ }, "flow_title": "Motion Blinds", "step": { + "connect": { + "data": { + "api_key": "Kl\u00ed\u010d API" + } + }, + "select": { + "data": { + "select_ip": "IP adresa" + } + }, "user": { "data": { "api_key": "Kl\u00ed\u010d API", "host": "IP adresa" }, - "description": "Budete pot\u0159ebovat 16m\u00edstn\u00fd API kl\u00ed\u010d, pokyny najdete na https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key", "title": "Motion Blinds" } } diff --git a/homeassistant/components/motion_blinds/translations/de.json b/homeassistant/components/motion_blinds/translations/de.json index dd1acc230f1..c1a7ac0bc8d 100644 --- a/homeassistant/components/motion_blinds/translations/de.json +++ b/homeassistant/components/motion_blinds/translations/de.json @@ -1,16 +1,28 @@ { "config": { "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "connection_error": "Verbindung fehlgeschlagen" }, "flow_title": "Jalousien", "step": { + "connect": { + "data": { + "api_key": "API-Schl\u00fcssel" + } + }, + "select": { + "data": { + "select_ip": "IP-Adresse" + } + }, "user": { "data": { "api_key": "API-Schl\u00fcssel", "host": "IP-Adresse" }, - "description": "Ein 16-Zeichen-API-Schl\u00fcssel wird ben\u00f6tigt, siehe https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key", + "description": "Stelle eine Verbindung zu deinem Motion Gateway her. Wenn die IP-Adresse leer bleibt, wird die automatische Erkennung verwendet", "title": "Jalousien" } } diff --git a/homeassistant/components/motion_blinds/translations/en.json b/homeassistant/components/motion_blinds/translations/en.json index b7830a255fc..3a968bc6491 100644 --- a/homeassistant/components/motion_blinds/translations/en.json +++ b/homeassistant/components/motion_blinds/translations/en.json @@ -5,14 +5,31 @@ "already_in_progress": "Configuration flow is already in progress", "connection_error": "Failed to connect" }, + "error": { + "discovery_error": "Failed to discover a Motion Gateway" + }, "flow_title": "Motion Blinds", "step": { + "connect": { + "data": { + "api_key": "API Key" + }, + "description": "You will need the 16 character API Key, see https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instructions", + "title": "Motion Blinds" + }, + "select": { + "data": { + "select_ip": "IP Address" + }, + "description": "Run the setup again if you want to connect additional Motion Gateways", + "title": "Select the Motion Gateway that you wish to connect" + }, "user": { "data": { "api_key": "API Key", "host": "IP Address" }, - "description": "You will need the 16 character API Key, see https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instructions", + "description": "Connect to your Motion Gateway, if the IP address is not set, auto-discovery is used", "title": "Motion Blinds" } } diff --git a/homeassistant/components/motion_blinds/translations/es.json b/homeassistant/components/motion_blinds/translations/es.json index bac5ffddbd3..7d7c6c1510f 100644 --- a/homeassistant/components/motion_blinds/translations/es.json +++ b/homeassistant/components/motion_blinds/translations/es.json @@ -5,14 +5,31 @@ "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", "connection_error": "No se pudo conectar" }, + "error": { + "discovery_error": "No se pudo descubrir un detector de movimiento" + }, "flow_title": "Motion Blinds", "step": { + "connect": { + "data": { + "api_key": "Clave API" + }, + "description": "Necesitar\u00e1 la clave de API de 16 caracteres, consulte https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key para obtener instrucciones", + "title": "Estores motorizados" + }, + "select": { + "data": { + "select_ip": "Direcci\u00f3n IP" + }, + "description": "Ejecute la configuraci\u00f3n de nuevo si desea conectar detectores de movimiento adicionales", + "title": "Selecciona el detector de Movimiento que deseas conectar" + }, "user": { "data": { "api_key": "Clave API", "host": "Direcci\u00f3n IP" }, - "description": "Necesitar\u00e1s la Clave API de 16 caracteres, consulta https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key para instrucciones", + "description": "Con\u00e9ctate a tu Motion Gateway, si la direcci\u00f3n IP no est\u00e1 establecida, se utilitzar\u00e1 la detecci\u00f3n autom\u00e1tica", "title": "Motion Blinds" } } diff --git a/homeassistant/components/motion_blinds/translations/et.json b/homeassistant/components/motion_blinds/translations/et.json index b55640d8905..5e585dec1a3 100644 --- a/homeassistant/components/motion_blinds/translations/et.json +++ b/homeassistant/components/motion_blinds/translations/et.json @@ -5,14 +5,31 @@ "already_in_progress": "Seadistamine on juba k\u00e4imas", "connection_error": "\u00dchendamine nurjus" }, + "error": { + "discovery_error": "Motion Gateway avastamine nurjus" + }, "flow_title": "", "step": { + "connect": { + "data": { + "api_key": "API v\u00f5ti" + }, + "description": "On vaja 16-kohalist API-v\u00f5tit, juhiste saamiseks vaata https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key", + "title": "" + }, + "select": { + "data": { + "select_ip": "IP aadress" + }, + "description": "K\u00e4ivita seadistamine uuesti kui soovid \u00fchendada t\u00e4iendavaid Motion Gateway sidumisi", + "title": "Vali Motion Gateway, mille soovid \u00fchendada" + }, "user": { "data": { "api_key": "API v\u00f5ti", "host": "IP-aadress" }, - "description": "Vaja on 16-kohalist API-v\u00f5tit. Juhiste saamiseks vt https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key", + "description": "\u00dchenda oma Motion Gatewayga. Kui IP-aadress on m\u00e4\u00e4ramata kasutatakse automaatset avastamist", "title": "" } } diff --git a/homeassistant/components/motion_blinds/translations/fr.json b/homeassistant/components/motion_blinds/translations/fr.json new file mode 100644 index 00000000000..86d008b9e6d --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/fr.json @@ -0,0 +1,17 @@ +{ + "config": { + "step": { + "connect": { + "data": { + "api_key": "Cl\u00e9 API" + }, + "description": "Vous aurez besoin de la cl\u00e9 API de 16 caract\u00e8res, voir https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key pour les instructions" + }, + "select": { + "data": { + "select_ip": "Adresse IP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/it.json b/homeassistant/components/motion_blinds/translations/it.json index ff56f184ac2..1d79ae28ee5 100644 --- a/homeassistant/components/motion_blinds/translations/it.json +++ b/homeassistant/components/motion_blinds/translations/it.json @@ -5,14 +5,31 @@ "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", "connection_error": "Impossibile connettersi" }, + "error": { + "discovery_error": "Impossibile rilevare un Motion Gateway" + }, "flow_title": "Tende Motion", "step": { + "connect": { + "data": { + "api_key": "Chiave API" + }, + "description": "Avrai bisogno della chiave API di 16 caratteri, consulta https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key per le istruzioni", + "title": "Motion Blinds" + }, + "select": { + "data": { + "select_ip": "Indirizzo IP" + }, + "description": "Esegui nuovamente l'installazione se desideri collegare altri Motion Gateway", + "title": "Seleziona il Motion Gateway che vorresti collegare" + }, "user": { "data": { "api_key": "Chiave API", "host": "Indirizzo IP" }, - "description": "Avrai bisogno della chiave API di 16 caratteri, consulta https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key per le istruzioni", + "description": "Connetti il tuo Motion Gateway, se l'indirizzo IP non \u00e8 impostato, sar\u00e0 utilizzato il rilevamento automatico", "title": "Tende Motion" } } diff --git a/homeassistant/components/motion_blinds/translations/lb.json b/homeassistant/components/motion_blinds/translations/lb.json index 7a3dcfdbf07..85caeea79e5 100644 --- a/homeassistant/components/motion_blinds/translations/lb.json +++ b/homeassistant/components/motion_blinds/translations/lb.json @@ -5,7 +5,21 @@ "already_in_progress": "Konfiguratioun's Oflaf ass schon am gaang", "connection_error": "Feeler beim verbannen" }, + "error": { + "discovery_error": "Feeler beim Entdecken vun enger Motion Gateway" + }, "step": { + "connect": { + "data": { + "api_key": "API Schl\u00ebssel" + }, + "description": "Du brauchs de 16 stellegen API Schl\u00ebssel, kuck https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key fir w\u00e9ider Instruktiounen" + }, + "select": { + "data": { + "select_ip": "IP Adresse" + } + }, "user": { "data": { "api_key": "API Schl\u00ebssel", diff --git a/homeassistant/components/motion_blinds/translations/no.json b/homeassistant/components/motion_blinds/translations/no.json index 9e406150691..e86da7c1fc4 100644 --- a/homeassistant/components/motion_blinds/translations/no.json +++ b/homeassistant/components/motion_blinds/translations/no.json @@ -5,14 +5,31 @@ "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", "connection_error": "Tilkobling mislyktes" }, + "error": { + "discovery_error": "Kunne ikke oppdage en Motion Gateway" + }, "flow_title": "Motion Blinds", "step": { + "connect": { + "data": { + "api_key": "API-n\u00f8kkel" + }, + "description": "Du trenger API-n\u00f8kkelen med 16 tegn, se https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instruksjoner", + "title": "" + }, + "select": { + "data": { + "select_ip": "IP adresse" + }, + "description": "Kj\u00f8r oppsettet p\u00e5 nytt hvis du vil koble til flere Motion Gateways", + "title": "Velg Motion Gateway som du vil koble til" + }, "user": { "data": { "api_key": "API-n\u00f8kkel", "host": "IP adresse" }, - "description": "Du trenger API-n\u00f8kkelen med 16 tegn, se https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key for instruksjoner", + "description": "Koble til Motion Gateway. Hvis IP-adressen ikke er angitt, brukes automatisk oppdagelse", "title": "Motion Blinds" } } diff --git a/homeassistant/components/motion_blinds/translations/pl.json b/homeassistant/components/motion_blinds/translations/pl.json index 8f73496fd1d..1d34d22d65e 100644 --- a/homeassistant/components/motion_blinds/translations/pl.json +++ b/homeassistant/components/motion_blinds/translations/pl.json @@ -5,14 +5,31 @@ "already_in_progress": "Konfiguracja jest ju\u017c w toku", "connection_error": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, + "error": { + "discovery_error": "Nie uda\u0142o si\u0119 wykry\u0107 bramki ruchu" + }, "flow_title": "Motion Blinds", "step": { + "connect": { + "data": { + "api_key": "Klucz API" + }, + "description": "B\u0119dziesz potrzebowa\u0142 16-znakowego klucza API, instrukcje znajdziesz na https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key", + "title": "Motion Blinds" + }, + "select": { + "data": { + "select_ip": "Adres IP" + }, + "description": "Uruchom ponownie konfiguracj\u0119, je\u015bli chcesz pod\u0142\u0105czy\u0107 dodatkowe bramki ruchu", + "title": "Wybierz bram\u0119 ruchu, z kt\u00f3r\u0105 chcesz si\u0119 po\u0142\u0105czy\u0107" + }, "user": { "data": { "api_key": "Klucz API", "host": "Adres IP" }, - "description": "B\u0119dziesz potrzebowa\u0142 16-znakowego klucza API, instrukcje znajdziesz na https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key", + "description": "Po\u0142\u0105cz si\u0119 z bram\u0105 ruchu. Je\u015bli adres IP nie jest ustawiony, u\u017cywane jest automatyczne wykrywanie", "title": "Motion Blinds" } } diff --git a/homeassistant/components/motion_blinds/translations/pt.json b/homeassistant/components/motion_blinds/translations/pt.json index fe188057e46..64ccd6061d2 100644 --- a/homeassistant/components/motion_blinds/translations/pt.json +++ b/homeassistant/components/motion_blinds/translations/pt.json @@ -5,12 +5,25 @@ "already_in_progress": "O processo de configura\u00e7\u00e3o j\u00e1 est\u00e1 a decorrer", "connection_error": "Falha na liga\u00e7\u00e3o" }, + "flow_title": "Cortinas Motion", "step": { + "connect": { + "data": { + "api_key": "API Key" + }, + "title": "Cortinas Motion" + }, + "select": { + "data": { + "select_ip": "Endere\u00e7o IP" + } + }, "user": { "data": { "api_key": "API Key", "host": "Endere\u00e7o IP" - } + }, + "title": "Cortinas Motion" } } } diff --git a/homeassistant/components/motion_blinds/translations/ru.json b/homeassistant/components/motion_blinds/translations/ru.json index 1a249a4fab8..ae2d3229c20 100644 --- a/homeassistant/components/motion_blinds/translations/ru.json +++ b/homeassistant/components/motion_blinds/translations/ru.json @@ -5,14 +5,31 @@ "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, + "error": { + "discovery_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c \u0448\u043b\u044e\u0437 Motion." + }, "flow_title": "Motion Blinds", "step": { + "connect": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "description": "\u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c 16-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key.", + "title": "Motion Blinds" + }, + "select": { + "data": { + "select_ip": "IP-\u0430\u0434\u0440\u0435\u0441" + }, + "description": "\u0417\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 \u0435\u0449\u0451 \u0440\u0430\u0437, \u0435\u0441\u043b\u0438 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0435\u0449\u0451 \u043e\u0434\u0438\u043d \u0448\u043b\u044e\u0437", + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0448\u043b\u044e\u0437 Motion" + }, "user": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API", "host": "IP-\u0430\u0434\u0440\u0435\u0441" }, - "description": "\u041e \u0442\u043e\u043c, \u043a\u0430\u043a \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c 16-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u0443\u0437\u043d\u0430\u0442\u044c \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0438 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key.", + "description": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0448\u043b\u044e\u0437\u0443 Motion. \u0414\u043b\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0433\u043e \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u044f \u0448\u043b\u044e\u0437\u0430, \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u043e\u043b\u0435 IP-\u0430\u0434\u0440\u0435\u0441\u0430 \u043f\u0443\u0441\u0442\u044b\u043c.", "title": "Motion Blinds" } } diff --git a/homeassistant/components/motion_blinds/translations/tr.json b/homeassistant/components/motion_blinds/translations/tr.json index 545a3547ffc..194608780c9 100644 --- a/homeassistant/components/motion_blinds/translations/tr.json +++ b/homeassistant/components/motion_blinds/translations/tr.json @@ -1,14 +1,30 @@ { "config": { "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", "connection_error": "Ba\u011flanma hatas\u0131" }, + "flow_title": "Hareketli Panjurlar", "step": { + "connect": { + "data": { + "api_key": "API Anahtar\u0131" + } + }, + "select": { + "data": { + "select_ip": "\u0130p Adresi" + }, + "title": "Ba\u011flamak istedi\u011finiz Hareket A\u011f Ge\u00e7idini se\u00e7in" + }, "user": { "data": { "api_key": "API Anahtar\u0131", "host": "IP adresi" - } + }, + "description": "Motion Gateway'inize ba\u011flan\u0131n, IP adresi ayarlanmad\u0131ysa, otomatik ke\u015fif kullan\u0131l\u0131r", + "title": "Hareketli Panjurlar" } } } diff --git a/homeassistant/components/motion_blinds/translations/uk.json b/homeassistant/components/motion_blinds/translations/uk.json new file mode 100644 index 00000000000..99ccb60dc6c --- /dev/null +++ b/homeassistant/components/motion_blinds/translations/uk.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "connection_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "error": { + "discovery_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0432\u0438\u044f\u0432\u0438\u0442\u0438 Motion Gateway" + }, + "flow_title": "Motion Blinds", + "step": { + "connect": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "description": "\u0412\u0430\u043c \u043f\u043e\u0442\u0440\u0456\u0431\u0435\u043d 16-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API, \u0434\u0438\u0432. https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0439", + "title": "Motion Blinds" + }, + "select": { + "data": { + "select_ip": "IP-\u0430\u0434\u0440\u0435\u0441\u0430" + }, + "description": "\u0417\u0430\u043f\u0443\u0441\u0442\u0456\u0442\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0443 \u0449\u0435 \u0440\u0430\u0437, \u044f\u043a\u0449\u043e \u0432\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0456 Motion Gateway", + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c Motion Gateway, \u044f\u043a\u0438\u0439 \u0432\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438" + }, + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430" + }, + "description": "\u041f\u0440\u043e \u0442\u0435, \u044f\u043a \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 16-\u0441\u0438\u043c\u0432\u043e\u043b\u044c\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API, \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0456\u0437\u043d\u0430\u0442\u0438\u0441\u044f \u0432 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0457 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key.", + "title": "Motion Blinds" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/motion_blinds/translations/zh-Hant.json b/homeassistant/components/motion_blinds/translations/zh-Hant.json index 37925ca6288..0f2f9881ebd 100644 --- a/homeassistant/components/motion_blinds/translations/zh-Hant.json +++ b/homeassistant/components/motion_blinds/translations/zh-Hant.json @@ -5,14 +5,31 @@ "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", "connection_error": "\u9023\u7dda\u5931\u6557" }, + "error": { + "discovery_error": "\u63a2\u7d22 Motion \u9598\u9053\u5668\u5931\u6557" + }, "flow_title": "Motion Blinds", "step": { + "connect": { + "data": { + "api_key": "API \u5bc6\u9470" + }, + "description": "\u5c07\u9700\u8981\u8f38\u5165 16 \u4f4d\u5b57\u5143 API \u5bc6\u9470\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key \u4ee5\u7372\u5f97\u7372\u53d6\u5bc6\u9470\u7684\u6559\u5b78\u3002", + "title": "Motion Blinds" + }, + "select": { + "data": { + "select_ip": "IP \u4f4d\u5740" + }, + "description": "\u5047\u5982\u6b32\u9023\u7dda\u81f3\u5176\u4ed6 Motion \u9598\u9053\u5668\uff0c\u8acb\u518d\u57f7\u884c\u4e00\u6b21\u8a2d\u5b9a\u6b65\u9a5f", + "title": "\u9078\u64c7\u6240\u8981\u9023\u7dda\u7684 Motion \u7db2\u95dc" + }, "user": { "data": { "api_key": "API \u5bc6\u9470", "host": "IP \u4f4d\u5740" }, - "description": "\u5c07\u9700\u8981\u8f38\u5165 16 \u4f4d\u5b57\u5143 API \u5bc6\u9470\uff0c\u8acb\u53c3\u95b1 https://www.home-assistant.io/integrations/motion_blinds/#retrieving-the-key \u4ee5\u7372\u5f97\u7372\u53d6\u5bc6\u9470\u7684\u6559\u5b78\u3002", + "description": "\u9023\u7dda\u81f3 Motion \u9598\u9053\u5668\uff0c\u5047\u5982\u672a\u63d0\u4f9b IP \u4f4d\u5740\uff0c\u5c07\u4f7f\u7528\u81ea\u52d5\u63a2\u7d22", "title": "Motion Blinds" } } diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 1273b720dd8..6685347b3e3 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -105,7 +105,7 @@ class MpdDevice(MediaPlayerEntity): self._currentplaylist = None self._is_connected = False self._muted = False - self._muted_volume = 0 + self._muted_volume = None self._media_position_updated_at = None self._media_position = None self._commands = None @@ -401,7 +401,7 @@ class MpdDevice(MediaPlayerEntity): if mute: self._muted_volume = self.volume_level await self.async_set_volume_level(0) - else: + elif self._muted_volume is not None: await self.async_set_volume_level(self._muted_volume) self._muted = mute diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index cced3670cca..788f8d1957e 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -3,7 +3,6 @@ import asyncio from functools import lru_cache, partial, wraps import inspect from itertools import groupby -import json import logging from operator import attrgetter import os @@ -20,8 +19,6 @@ from homeassistant import config_entries from homeassistant.components import websocket_api from homeassistant.const import ( CONF_CLIENT_ID, - CONF_DEVICE, - CONF_NAME, CONF_PASSWORD, CONF_PAYLOAD, CONF_PORT, @@ -36,7 +33,6 @@ from homeassistant.core import CoreState, Event, HassJob, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.helpers import config_validation as cv, event, template from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send -from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceDataType from homeassistant.loader import bind_hass from homeassistant.util import dt as dt_util @@ -47,9 +43,6 @@ from homeassistant.util.logging import catch_log_exception from . import config_flow # noqa: F401 pylint: disable=unused-import from . import debug_info, discovery from .const import ( - ATTR_DISCOVERY_HASH, - ATTR_DISCOVERY_PAYLOAD, - ATTR_DISCOVERY_TOPIC, ATTR_PAYLOAD, ATTR_QOS, ATTR_RETAIN, @@ -64,8 +57,6 @@ from .const import ( DATA_MQTT_CONFIG, DEFAULT_BIRTH, DEFAULT_DISCOVERY, - DEFAULT_PAYLOAD_AVAILABLE, - DEFAULT_PAYLOAD_NOT_AVAILABLE, DEFAULT_PREFIX, DEFAULT_QOS, DEFAULT_RETAIN, @@ -75,15 +66,8 @@ from .const import ( MQTT_DISCONNECTED, PROTOCOL_311, ) -from .debug_info import log_messages -from .discovery import ( - LAST_DISCOVERY, - MQTT_DISCOVERY_UPDATED, - clear_discovery_hash, - set_discovery_hash, -) +from .discovery import LAST_DISCOVERY from .models import Message, MessageCallbackType, PublishPayloadType -from .subscription import async_subscribe_topics, async_unsubscribe_topics from .util import _VALID_QOS_SCHEMA, valid_publish_topic, valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -103,20 +87,6 @@ CONF_TLS_VERSION = "tls_version" CONF_COMMAND_TOPIC = "command_topic" CONF_TOPIC = "topic" -CONF_AVAILABILITY = "availability" -CONF_AVAILABILITY_TOPIC = "availability_topic" -CONF_PAYLOAD_AVAILABLE = "payload_available" -CONF_PAYLOAD_NOT_AVAILABLE = "payload_not_available" -CONF_JSON_ATTRS_TOPIC = "json_attributes_topic" -CONF_JSON_ATTRS_TEMPLATE = "json_attributes_template" - -CONF_IDENTIFIERS = "identifiers" -CONF_CONNECTIONS = "connections" -CONF_MANUFACTURER = "manufacturer" -CONF_MODEL = "model" -CONF_SW_VERSION = "sw_version" -CONF_VIA_DEVICE = "via_device" -CONF_DEPRECATED_VIA_HUB = "via_hub" PROTOCOL_31 = "3.1" @@ -145,6 +115,7 @@ PLATFORMS = [ "fan", "light", "lock", + "number", "scene", "sensor", "switch", @@ -152,16 +123,6 @@ PLATFORMS = [ ] -def validate_device_has_at_least_one_identifier(value: ConfigType) -> ConfigType: - """Validate that a device info entry has at least one identifying value.""" - if not value.get(CONF_IDENTIFIERS) and not value.get(CONF_CONNECTIONS): - raise vol.Invalid( - "Device must have at least one identifying value in " - "'identifiers' and/or 'connections'" - ) - return value - - CLIENT_KEY_AUTH_MSG = ( "client_key and client_cert must both be present in " "the MQTT broker configuration" @@ -237,69 +198,6 @@ CONFIG_SCHEMA = vol.Schema( SCHEMA_BASE = {vol.Optional(CONF_QOS, default=DEFAULT_QOS): _VALID_QOS_SCHEMA} -MQTT_AVAILABILITY_SINGLE_SCHEMA = vol.Schema( - { - vol.Exclusive(CONF_AVAILABILITY_TOPIC, "availability"): valid_subscribe_topic, - vol.Optional( - CONF_PAYLOAD_AVAILABLE, default=DEFAULT_PAYLOAD_AVAILABLE - ): cv.string, - vol.Optional( - CONF_PAYLOAD_NOT_AVAILABLE, default=DEFAULT_PAYLOAD_NOT_AVAILABLE - ): cv.string, - } -) - -MQTT_AVAILABILITY_LIST_SCHEMA = vol.Schema( - { - vol.Exclusive(CONF_AVAILABILITY, "availability"): vol.All( - cv.ensure_list, - [ - { - vol.Optional(CONF_TOPIC): valid_subscribe_topic, - vol.Optional( - CONF_PAYLOAD_AVAILABLE, default=DEFAULT_PAYLOAD_AVAILABLE - ): cv.string, - vol.Optional( - CONF_PAYLOAD_NOT_AVAILABLE, - default=DEFAULT_PAYLOAD_NOT_AVAILABLE, - ): cv.string, - } - ], - ), - } -) - -MQTT_AVAILABILITY_SCHEMA = MQTT_AVAILABILITY_SINGLE_SCHEMA.extend( - MQTT_AVAILABILITY_LIST_SCHEMA.schema -) - -MQTT_ENTITY_DEVICE_INFO_SCHEMA = vol.All( - cv.deprecated(CONF_DEPRECATED_VIA_HUB, CONF_VIA_DEVICE), - vol.Schema( - { - vol.Optional(CONF_IDENTIFIERS, default=list): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Optional(CONF_CONNECTIONS, default=list): vol.All( - cv.ensure_list, [vol.All(vol.Length(2), [cv.string])] - ), - vol.Optional(CONF_MANUFACTURER): cv.string, - vol.Optional(CONF_MODEL): cv.string, - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_SW_VERSION): cv.string, - vol.Optional(CONF_VIA_DEVICE): cv.string, - } - ), - validate_device_has_at_least_one_identifier, -) - -MQTT_JSON_ATTRS_SCHEMA = vol.Schema( - { - vol.Optional(CONF_JSON_ATTRS_TOPIC): valid_subscribe_topic, - vol.Optional(CONF_JSON_ATTRS_TEMPLATE): cv.template, - } -) - MQTT_BASE_PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend(SCHEMA_BASE) # Sensor type platforms subscribe to MQTT events @@ -967,7 +865,7 @@ class MQTT: "Received message on %s%s: %s", msg.topic, " (retained)" if msg.retain else "", - msg.payload, + msg.payload[0:8192], ) timestamp = dt_util.utcnow() @@ -982,7 +880,7 @@ class MQTT: except (AttributeError, UnicodeDecodeError): _LOGGER.warning( "Can't decode payload %s on %s with encoding %s (for %s)", - msg.payload, + msg.payload[0:8192], msg.topic, subscription.encoding, subscription.job, @@ -1079,335 +977,6 @@ def _matcher_for_topic(subscription: str) -> Any: return lambda topic: next(matcher.iter_match(topic), False) -class MqttAttributes(Entity): - """Mixin used for platforms that support JSON attributes.""" - - def __init__(self, config: dict) -> None: - """Initialize the JSON attributes mixin.""" - self._attributes = None - self._attributes_sub_state = None - self._attributes_config = config - - async def async_added_to_hass(self) -> None: - """Subscribe MQTT events.""" - await super().async_added_to_hass() - await self._attributes_subscribe_topics() - - async def attributes_discovery_update(self, config: dict): - """Handle updated discovery message.""" - self._attributes_config = config - await self._attributes_subscribe_topics() - - async def _attributes_subscribe_topics(self): - """(Re)Subscribe to topics.""" - attr_tpl = self._attributes_config.get(CONF_JSON_ATTRS_TEMPLATE) - if attr_tpl is not None: - attr_tpl.hass = self.hass - - @callback - @log_messages(self.hass, self.entity_id) - def attributes_message_received(msg: Message) -> None: - try: - payload = msg.payload - if attr_tpl is not None: - payload = attr_tpl.async_render_with_possible_json_value(payload) - json_dict = json.loads(payload) - if isinstance(json_dict, dict): - self._attributes = json_dict - self.async_write_ha_state() - else: - _LOGGER.warning("JSON result was not a dictionary") - self._attributes = None - except ValueError: - _LOGGER.warning("Erroneous JSON: %s", payload) - self._attributes = None - - self._attributes_sub_state = await async_subscribe_topics( - self.hass, - self._attributes_sub_state, - { - CONF_JSON_ATTRS_TOPIC: { - "topic": self._attributes_config.get(CONF_JSON_ATTRS_TOPIC), - "msg_callback": attributes_message_received, - "qos": self._attributes_config.get(CONF_QOS), - } - }, - ) - - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._attributes_sub_state = await async_unsubscribe_topics( - self.hass, self._attributes_sub_state - ) - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._attributes - - -class MqttAvailability(Entity): - """Mixin used for platforms that report availability.""" - - def __init__(self, config: dict) -> None: - """Initialize the availability mixin.""" - self._availability_sub_state = None - self._available = False - self._availability_setup_from_config(config) - - async def async_added_to_hass(self) -> None: - """Subscribe MQTT events.""" - await super().async_added_to_hass() - await self._availability_subscribe_topics() - self.async_on_remove( - async_dispatcher_connect(self.hass, MQTT_CONNECTED, self.async_mqtt_connect) - ) - self.async_on_remove( - async_dispatcher_connect( - self.hass, MQTT_DISCONNECTED, self.async_mqtt_connect - ) - ) - - async def availability_discovery_update(self, config: dict): - """Handle updated discovery message.""" - self._availability_setup_from_config(config) - await self._availability_subscribe_topics() - - def _availability_setup_from_config(self, config): - """(Re)Setup.""" - self._avail_topics = {} - if CONF_AVAILABILITY_TOPIC in config: - self._avail_topics[config[CONF_AVAILABILITY_TOPIC]] = { - CONF_PAYLOAD_AVAILABLE: config[CONF_PAYLOAD_AVAILABLE], - CONF_PAYLOAD_NOT_AVAILABLE: config[CONF_PAYLOAD_NOT_AVAILABLE], - } - - if CONF_AVAILABILITY in config: - for avail in config[CONF_AVAILABILITY]: - self._avail_topics[avail[CONF_TOPIC]] = { - CONF_PAYLOAD_AVAILABLE: avail[CONF_PAYLOAD_AVAILABLE], - CONF_PAYLOAD_NOT_AVAILABLE: avail[CONF_PAYLOAD_NOT_AVAILABLE], - } - - self._avail_config = config - - async def _availability_subscribe_topics(self): - """(Re)Subscribe to topics.""" - - @callback - @log_messages(self.hass, self.entity_id) - def availability_message_received(msg: Message) -> None: - """Handle a new received MQTT availability message.""" - topic = msg.topic - if msg.payload == self._avail_topics[topic][CONF_PAYLOAD_AVAILABLE]: - self._available = True - elif msg.payload == self._avail_topics[topic][CONF_PAYLOAD_NOT_AVAILABLE]: - self._available = False - - self.async_write_ha_state() - - topics = {} - for topic in self._avail_topics: - topics[f"availability_{topic}"] = { - "topic": topic, - "msg_callback": availability_message_received, - "qos": self._avail_config[CONF_QOS], - } - - self._availability_sub_state = await async_subscribe_topics( - self.hass, - self._availability_sub_state, - topics, - ) - - @callback - def async_mqtt_connect(self): - """Update state on connection/disconnection to MQTT broker.""" - if not self.hass.is_stopping: - self.async_write_ha_state() - - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._availability_sub_state = await async_unsubscribe_topics( - self.hass, self._availability_sub_state - ) - - @property - def available(self) -> bool: - """Return if the device is available.""" - if not self.hass.data[DATA_MQTT].connected and not self.hass.is_stopping: - return False - return not self._avail_topics or self._available - - -async def cleanup_device_registry(hass, device_id): - """Remove device registry entry if there are no remaining entities or triggers.""" - # Local import to avoid circular dependencies - # pylint: disable=import-outside-toplevel - from . import device_trigger, tag - - device_registry = await hass.helpers.device_registry.async_get_registry() - entity_registry = await hass.helpers.entity_registry.async_get_registry() - if ( - device_id - and not hass.helpers.entity_registry.async_entries_for_device( - entity_registry, device_id, include_disabled_entities=True - ) - and not await device_trigger.async_get_triggers(hass, device_id) - and not tag.async_has_tags(hass, device_id) - ): - device_registry.async_remove_device(device_id) - - -class MqttDiscoveryUpdate(Entity): - """Mixin used to handle updated discovery message.""" - - def __init__(self, discovery_data, discovery_update=None) -> None: - """Initialize the discovery update mixin.""" - self._discovery_data = discovery_data - self._discovery_update = discovery_update - self._remove_signal = None - self._removed_from_hass = False - - async def async_added_to_hass(self) -> None: - """Subscribe to discovery updates.""" - await super().async_added_to_hass() - self._removed_from_hass = False - discovery_hash = ( - self._discovery_data[ATTR_DISCOVERY_HASH] if self._discovery_data else None - ) - - async def _async_remove_state_and_registry_entry(self) -> None: - """Remove entity's state and entity registry entry. - - Remove entity from entity registry if it is registered, this also removes the state. - If the entity is not in the entity registry, just remove the state. - """ - entity_registry = ( - await self.hass.helpers.entity_registry.async_get_registry() - ) - if entity_registry.async_is_registered(self.entity_id): - entity_entry = entity_registry.async_get(self.entity_id) - entity_registry.async_remove(self.entity_id) - await cleanup_device_registry(self.hass, entity_entry.device_id) - else: - await self.async_remove() - - async def discovery_callback(payload): - """Handle discovery update.""" - _LOGGER.info( - "Got update for entity with hash: %s '%s'", - discovery_hash, - payload, - ) - old_payload = self._discovery_data[ATTR_DISCOVERY_PAYLOAD] - debug_info.update_entity_discovery_data(self.hass, payload, self.entity_id) - if not payload: - # Empty payload: Remove component - _LOGGER.info("Removing component: %s", self.entity_id) - self._cleanup_discovery_on_remove() - await _async_remove_state_and_registry_entry(self) - elif self._discovery_update: - if old_payload != self._discovery_data[ATTR_DISCOVERY_PAYLOAD]: - # Non-empty, changed payload: Notify component - _LOGGER.info("Updating component: %s", self.entity_id) - await self._discovery_update(payload) - else: - # Non-empty, unchanged payload: Ignore to avoid changing states - _LOGGER.info("Ignoring unchanged update for: %s", self.entity_id) - - if discovery_hash: - debug_info.add_entity_discovery_data( - self.hass, self._discovery_data, self.entity_id - ) - # Set in case the entity has been removed and is re-added, for example when changing entity_id - set_discovery_hash(self.hass, discovery_hash) - self._remove_signal = async_dispatcher_connect( - self.hass, - MQTT_DISCOVERY_UPDATED.format(discovery_hash), - discovery_callback, - ) - - async def async_removed_from_registry(self) -> None: - """Clear retained discovery topic in broker.""" - if not self._removed_from_hass: - discovery_topic = self._discovery_data[ATTR_DISCOVERY_TOPIC] - publish( - self.hass, - discovery_topic, - "", - retain=True, - ) - - async def async_will_remove_from_hass(self) -> None: - """Stop listening to signal and cleanup discovery data..""" - self._cleanup_discovery_on_remove() - - def _cleanup_discovery_on_remove(self) -> None: - """Stop listening to signal and cleanup discovery data.""" - if self._discovery_data and not self._removed_from_hass: - debug_info.remove_entity_data(self.hass, self.entity_id) - clear_discovery_hash(self.hass, self._discovery_data[ATTR_DISCOVERY_HASH]) - self._removed_from_hass = True - - if self._remove_signal: - self._remove_signal() - self._remove_signal = None - - -def device_info_from_config(config): - """Return a device description for device registry.""" - if not config: - return None - - info = { - "identifiers": {(DOMAIN, id_) for id_ in config[CONF_IDENTIFIERS]}, - "connections": {tuple(x) for x in config[CONF_CONNECTIONS]}, - } - - if CONF_MANUFACTURER in config: - info["manufacturer"] = config[CONF_MANUFACTURER] - - if CONF_MODEL in config: - info["model"] = config[CONF_MODEL] - - if CONF_NAME in config: - info["name"] = config[CONF_NAME] - - if CONF_SW_VERSION in config: - info["sw_version"] = config[CONF_SW_VERSION] - - if CONF_VIA_DEVICE in config: - info["via_device"] = (DOMAIN, config[CONF_VIA_DEVICE]) - - return info - - -class MqttEntityDeviceInfo(Entity): - """Mixin used for mqtt platforms that support the device registry.""" - - def __init__(self, device_config: Optional[ConfigType], config_entry=None) -> None: - """Initialize the device mixin.""" - self._device_config = device_config - self._config_entry = config_entry - - async def device_info_discovery_update(self, config: dict): - """Handle updated discovery message.""" - self._device_config = config.get(CONF_DEVICE) - device_registry = await self.hass.helpers.device_registry.async_get_registry() - config_entry_id = self._config_entry.entry_id - device_info = self.device_info - - if config_entry_id is not None and device_info is not None: - device_info["config_entry_id"] = config_entry_id - device_registry.async_get_or_create(**device_info) - - @property - def device_info(self): - """Return a device description for device registry.""" - return device_info_from_config(self._device_config) - - @websocket_api.websocket_command( {vol.Required("type"): "mqtt/device/debug_info", vol.Required("device_id"): str} ) diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index c3f6b55e0fe..4b209f6f364 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -8,6 +8,7 @@ ABBREVIATIONS = { "aux_stat_tpl": "aux_state_template", "aux_stat_t": "aux_state_topic", "avty": "availability", + "avty_mode": "availability_mode", "avty_t": "availability_topic", "away_mode_cmd_t": "away_mode_command_topic", "away_mode_stat_tpl": "away_mode_state_template", @@ -54,11 +55,13 @@ ABBREVIATIONS = { "fx_tpl": "effect_template", "fx_val_tpl": "effect_value_template", "exp_aft": "expire_after", + "fan_mode_cmd_tpl": "fan_mode_command_template", "fan_mode_cmd_t": "fan_mode_command_topic", "fan_mode_stat_tpl": "fan_mode_state_template", "fan_mode_stat_t": "fan_mode_state_topic", "frc_upd": "force_update", "g_tpl": "green_template", + "hold_cmd_tpl": "hold_command_template", "hold_cmd_t": "hold_command_topic", "hold_stat_tpl": "hold_state_template", "hold_stat_t": "hold_state_topic", @@ -74,6 +77,7 @@ ABBREVIATIONS = { "min_mirs": "min_mireds", "max_temp": "max_temp", "min_temp": "min_temp", + "mode_cmd_tpl": "mode_command_template", "mode_cmd_t": "mode_command_topic", "mode_stat_tpl": "mode_state_template", "mode_stat_t": "mode_state_topic", @@ -150,13 +154,17 @@ ABBREVIATIONS = { "stat_val_tpl": "state_value_template", "stype": "subtype", "sup_feat": "supported_features", + "swing_mode_cmd_tpl": "swing_mode_command_template", "swing_mode_cmd_t": "swing_mode_command_topic", "swing_mode_stat_tpl": "swing_mode_state_template", "swing_mode_stat_t": "swing_mode_state_topic", + "temp_cmd_tpl": "temperature_command_template", "temp_cmd_t": "temperature_command_topic", + "temp_hi_cmd_tpl": "temperature_high_command_template", "temp_hi_cmd_t": "temperature_high_command_topic", "temp_hi_stat_tpl": "temperature_high_state_template", "temp_hi_stat_t": "temperature_high_state_topic", + "temp_lo_cmd_tpl": "temperature_low_command_template", "temp_lo_cmd_t": "temperature_low_command_topic", "temp_lo_stat_tpl": "temperature_low_state_template", "temp_lo_stat_t": "temperature_low_state_topic", diff --git a/homeassistant/components/mqtt/alarm_control_panel.py b/homeassistant/components/mqtt/alarm_control_panel.py index edf383a6819..38fec57607e 100644 --- a/homeassistant/components/mqtt/alarm_control_panel.py +++ b/homeassistant/components/mqtt/alarm_control_panel.py @@ -1,4 +1,5 @@ """This platform enables the possibility to control a MQTT alarm.""" +import functools import logging import re @@ -29,27 +30,27 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import ( - ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, subscription, ) from .. import mqtt from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, + MqttEntity, + async_setup_entry_helper, +) _LOGGER = logging.getLogger(__name__) @@ -79,7 +80,7 @@ PLATFORM_SCHEMA = ( CONF_COMMAND_TEMPLATE, default=DEFAULT_COMMAND_TEMPLATE ): cv.template, vol.Required(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PAYLOAD_ARM_AWAY, default=DEFAULT_ARM_AWAY): cv.string, vol.Optional(CONF_PAYLOAD_ARM_HOME, default=DEFAULT_ARM_HOME): cv.string, @@ -94,8 +95,8 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_VALUE_TEMPLATE): cv.template, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) ) @@ -104,76 +105,38 @@ async def async_setup_platform( ): """Set up MQTT alarm control panel through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, config, async_add_entities) + await _async_setup_entity(hass, async_add_entities, config) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT alarm control panel dynamically through MQTT discovery.""" - async def async_discover(discovery_payload): - """Discover and add an MQTT alarm control panel.""" - discovery_data = discovery_payload.discovery_data - try: - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity( - hass, config, async_add_entities, config_entry, discovery_data - ) - except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(alarm.DOMAIN, "mqtt"), async_discover + setup = functools.partial( + _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) + await async_setup_entry_helper(hass, alarm.DOMAIN, setup, PLATFORM_SCHEMA) async def _async_setup_entity( - hass, config, async_add_entities, config_entry=None, discovery_data=None + hass, async_add_entities, config, config_entry=None, discovery_data=None ): """Set up the MQTT Alarm Control Panel platform.""" async_add_entities([MqttAlarm(hass, config, config_entry, discovery_data)]) -class MqttAlarm( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - alarm.AlarmControlPanelEntity, -): +class MqttAlarm(MqttEntity, alarm.AlarmControlPanelEntity): """Representation of a MQTT alarm status.""" def __init__(self, hass, config, config_entry, discovery_data): """Init the MQTT Alarm Control Panel.""" - self.hass = hass self._state = None - self._unique_id = config.get(CONF_UNIQUE_ID) - self._sub_state = None - # Load config - self._setup_from_config(config) + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) - device_config = config.get(CONF_DEVICE) - - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) - - async def async_added_to_hass(self): - """Subscribe mqtt events.""" - await super().async_added_to_hass() - await self._subscribe_topics() - - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA(discovery_payload) - self._setup_from_config(config) - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA def _setup_from_config(self, config): self._config = config @@ -224,30 +187,11 @@ class MqttAlarm( }, ) - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - - @property - def should_poll(self): - """No polling needed.""" - return False - @property def name(self): """Return the name of the device.""" return self._config[CONF_NAME] - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - @property def state(self): """Return the state of the device.""" diff --git a/homeassistant/components/mqtt/binary_sensor.py b/homeassistant/components/mqtt/binary_sensor.py index e081423d590..d965401cef4 100644 --- a/homeassistant/components/mqtt/binary_sensor.py +++ b/homeassistant/components/mqtt/binary_sensor.py @@ -1,5 +1,6 @@ """Support for MQTT binary sensors.""" from datetime import timedelta +import functools import logging import voluptuous as vol @@ -21,28 +22,23 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.helpers.event as evt from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util import dt as dt_util -from . import ( - ATTR_DISCOVERY_HASH, - CONF_QOS, - CONF_STATE_TOPIC, - DOMAIN, - PLATFORMS, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - subscription, -) +from . import CONF_QOS, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, subscription from .. import mqtt from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, + MqttAvailability, + MqttEntity, + async_setup_entry_helper, +) _LOGGER = logging.getLogger(__name__) @@ -56,7 +52,7 @@ CONF_EXPIRE_AFTER = "expire_after" PLATFORM_SCHEMA = ( mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, @@ -67,8 +63,8 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_UNIQUE_ID): cv.string, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) ) @@ -77,51 +73,31 @@ async def async_setup_platform( ): """Set up MQTT binary sensor through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, config, async_add_entities) + await _async_setup_entity(hass, async_add_entities, config) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT binary sensor dynamically through MQTT discovery.""" - async def async_discover(discovery_payload): - """Discover and add a MQTT binary sensor.""" - discovery_data = discovery_payload.discovery_data - try: - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity( - hass, config, async_add_entities, config_entry, discovery_data - ) - except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(binary_sensor.DOMAIN, "mqtt"), async_discover + setup = functools.partial( + _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) + await async_setup_entry_helper(hass, binary_sensor.DOMAIN, setup, PLATFORM_SCHEMA) async def _async_setup_entity( - hass, config, async_add_entities, config_entry=None, discovery_data=None + hass, async_add_entities, config, config_entry=None, discovery_data=None ): """Set up the MQTT binary sensor.""" async_add_entities([MqttBinarySensor(hass, config, config_entry, discovery_data)]) -class MqttBinarySensor( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - BinarySensorEntity, -): +class MqttBinarySensor(MqttEntity, BinarySensorEntity): """Representation a binary sensor that is updated by MQTT.""" def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT binary sensor.""" - self.hass = hass - self._unique_id = config.get(CONF_UNIQUE_ID) self._state = None - self._sub_state = None self._expiration_trigger = None self._delay_listener = None expire_after = config.get(CONF_EXPIRE_AFTER) @@ -130,30 +106,12 @@ class MqttBinarySensor( else: self._expired = None - # Load config - self._setup_from_config(config) + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) - device_config = config.get(CONF_DEVICE) - - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) - - async def async_added_to_hass(self): - """Subscribe mqtt events.""" - await super().async_added_to_hass() - await self._subscribe_topics() - - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA(discovery_payload) - self._setup_from_config(config) - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA def _setup_from_config(self, config): self._config = config @@ -253,15 +211,6 @@ class MqttBinarySensor( }, ) - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - @callback def _value_is_expired(self, *_): """Triggered when value is expired.""" @@ -271,11 +220,6 @@ class MqttBinarySensor( self.async_write_ha_state() - @property - def should_poll(self): - """Return the polling state.""" - return False - @property def name(self): """Return the name of the binary sensor.""" @@ -296,11 +240,6 @@ class MqttBinarySensor( """Force update.""" return self._config[CONF_FORCE_UPDATE] - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - @property def available(self) -> bool: """Return true if the device is available and value has not expired.""" diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index e8783f74bd4..21fcb9276dd 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -1,4 +1,5 @@ """Camera that loads a picture from an MQTT topic.""" +import functools import logging import voluptuous as vol @@ -8,24 +9,19 @@ from homeassistant.components.camera import Camera from homeassistant.const import CONF_DEVICE, CONF_NAME, CONF_UNIQUE_ID from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from . import ( - ATTR_DISCOVERY_HASH, - CONF_QOS, - DOMAIN, - PLATFORMS, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - subscription, -) +from . import CONF_QOS, DOMAIN, PLATFORMS, subscription from .. import mqtt from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, + MqttEntity, + async_setup_entry_helper, +) _LOGGER = logging.getLogger(__name__) @@ -35,14 +31,14 @@ DEFAULT_NAME = "MQTT Camera" PLATFORM_SCHEMA = ( mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_UNIQUE_ID): cv.string, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) ) @@ -51,71 +47,42 @@ async def async_setup_platform( ): """Set up MQTT camera through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(config, async_add_entities) + await _async_setup_entity(async_add_entities, config) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT camera dynamically through MQTT discovery.""" - async def async_discover(discovery_payload): - """Discover and add a MQTT camera.""" - discovery_data = discovery_payload.discovery_data - try: - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity( - config, async_add_entities, config_entry, discovery_data - ) - except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(camera.DOMAIN, "mqtt"), async_discover + setup = functools.partial( + _async_setup_entity, async_add_entities, config_entry=config_entry ) + await async_setup_entry_helper(hass, camera.DOMAIN, setup, PLATFORM_SCHEMA) async def _async_setup_entity( - config, async_add_entities, config_entry=None, discovery_data=None + async_add_entities, config, config_entry=None, discovery_data=None ): """Set up the MQTT Camera.""" async_add_entities([MqttCamera(config, config_entry, discovery_data)]) -class MqttCamera( - MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, Camera -): +class MqttCamera(MqttEntity, Camera): """representation of a MQTT camera.""" def __init__(self, config, config_entry, discovery_data): """Initialize the MQTT Camera.""" - self._config = config - self._unique_id = config.get(CONF_UNIQUE_ID) - self._sub_state = None - self._last_image = None - device_config = config.get(CONF_DEVICE) - Camera.__init__(self) - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) + MqttEntity.__init__(self, None, config, config_entry, discovery_data) - async def async_added_to_hass(self): - """Subscribe MQTT events.""" - await super().async_added_to_hass() - await self._subscribe_topics() + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA(discovery_payload) + def _setup_from_config(self, config): self._config = config - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() async def _subscribe_topics(self): """(Re)Subscribe to topics.""" @@ -139,15 +106,6 @@ class MqttCamera( }, ) - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - async def async_camera_image(self): """Return image response.""" return self._last_image @@ -156,8 +114,3 @@ class MqttCamera( def name(self): """Return the name of this camera.""" return self._config[CONF_NAME] - - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id diff --git a/homeassistant/components/mqtt/climate.py b/homeassistant/components/mqtt/climate.py index c5835f8e7c7..15c7c916eeb 100644 --- a/homeassistant/components/mqtt/climate.py +++ b/homeassistant/components/mqtt/climate.py @@ -1,4 +1,5 @@ """Support for MQTT climate devices.""" +import functools import logging import voluptuous as vol @@ -47,26 +48,26 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import ( - ATTR_DISCOVERY_HASH, CONF_QOS, CONF_RETAIN, DOMAIN, MQTT_BASE_PLATFORM_SCHEMA, PLATFORMS, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, subscription, ) from .. import mqtt from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, + MqttEntity, + async_setup_entry_helper, +) _LOGGER = logging.getLogger(__name__) @@ -82,14 +83,17 @@ CONF_AWAY_MODE_STATE_TEMPLATE = "away_mode_state_template" CONF_AWAY_MODE_STATE_TOPIC = "away_mode_state_topic" CONF_CURRENT_TEMP_TEMPLATE = "current_temperature_template" CONF_CURRENT_TEMP_TOPIC = "current_temperature_topic" +CONF_FAN_MODE_COMMAND_TEMPLATE = "fan_mode_command_template" CONF_FAN_MODE_COMMAND_TOPIC = "fan_mode_command_topic" CONF_FAN_MODE_LIST = "fan_modes" CONF_FAN_MODE_STATE_TEMPLATE = "fan_mode_state_template" CONF_FAN_MODE_STATE_TOPIC = "fan_mode_state_topic" +CONF_HOLD_COMMAND_TEMPLATE = "hold_command_template" CONF_HOLD_COMMAND_TOPIC = "hold_command_topic" CONF_HOLD_STATE_TEMPLATE = "hold_state_template" CONF_HOLD_STATE_TOPIC = "hold_state_topic" CONF_HOLD_LIST = "hold_modes" +CONF_MODE_COMMAND_TEMPLATE = "mode_command_template" CONF_MODE_COMMAND_TOPIC = "mode_command_topic" CONF_MODE_LIST = "modes" CONF_MODE_STATE_TEMPLATE = "mode_state_template" @@ -101,14 +105,18 @@ CONF_POWER_STATE_TEMPLATE = "power_state_template" CONF_POWER_STATE_TOPIC = "power_state_topic" CONF_PRECISION = "precision" CONF_SEND_IF_OFF = "send_if_off" +CONF_SWING_MODE_COMMAND_TEMPLATE = "swing_mode_command_template" CONF_SWING_MODE_COMMAND_TOPIC = "swing_mode_command_topic" CONF_SWING_MODE_LIST = "swing_modes" CONF_SWING_MODE_STATE_TEMPLATE = "swing_mode_state_template" CONF_SWING_MODE_STATE_TOPIC = "swing_mode_state_topic" +CONF_TEMP_COMMAND_TEMPLATE = "temperature_command_template" CONF_TEMP_COMMAND_TOPIC = "temperature_command_topic" +CONF_TEMP_HIGH_COMMAND_TEMPLATE = "temperature_high_command_template" CONF_TEMP_HIGH_COMMAND_TOPIC = "temperature_high_command_topic" CONF_TEMP_HIGH_STATE_TEMPLATE = "temperature_high_state_template" CONF_TEMP_HIGH_STATE_TOPIC = "temperature_high_state_topic" +CONF_TEMP_LOW_COMMAND_TEMPLATE = "temperature_low_command_template" CONF_TEMP_LOW_COMMAND_TOPIC = "temperature_low_command_topic" CONF_TEMP_LOW_STATE_TEMPLATE = "temperature_low_state_template" CONF_TEMP_LOW_STATE_TOPIC = "temperature_low_state_topic" @@ -119,7 +127,7 @@ CONF_TEMP_MAX = "max_temp" CONF_TEMP_MIN = "min_temp" CONF_TEMP_STEP = "temp_step" -TEMPLATE_KEYS = ( +VALUE_TEMPLATE_KEYS = ( CONF_AUX_STATE_TEMPLATE, CONF_AWAY_MODE_STATE_TEMPLATE, CONF_CURRENT_TEMP_TEMPLATE, @@ -134,6 +142,16 @@ TEMPLATE_KEYS = ( CONF_TEMP_STATE_TEMPLATE, ) +COMMAND_TEMPLATE_KEYS = { + CONF_FAN_MODE_COMMAND_TEMPLATE, + CONF_HOLD_COMMAND_TEMPLATE, + CONF_MODE_COMMAND_TEMPLATE, + CONF_SWING_MODE_COMMAND_TEMPLATE, + CONF_TEMP_COMMAND_TEMPLATE, + CONF_TEMP_HIGH_COMMAND_TEMPLATE, + CONF_TEMP_LOW_COMMAND_TEMPLATE, +} + TOPIC_KEYS = ( CONF_AUX_COMMAND_TOPIC, CONF_AUX_STATE_TOPIC, @@ -171,7 +189,8 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_AWAY_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_CURRENT_TEMP_TEMPLATE): cv.template, vol.Optional(CONF_CURRENT_TEMP_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_FAN_MODE_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_FAN_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional( CONF_FAN_MODE_LIST, @@ -179,10 +198,12 @@ PLATFORM_SCHEMA = ( ): cv.ensure_list, vol.Optional(CONF_FAN_MODE_STATE_TEMPLATE): cv.template, vol.Optional(CONF_FAN_MODE_STATE_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_HOLD_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_HOLD_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_HOLD_STATE_TEMPLATE): cv.template, vol.Optional(CONF_HOLD_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_HOLD_LIST, default=list): cv.ensure_list, + vol.Optional(CONF_MODE_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional( CONF_MODE_LIST, @@ -210,6 +231,7 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_SEND_IF_OFF, default=True): cv.boolean, vol.Optional(CONF_ACTION_TEMPLATE): cv.template, vol.Optional(CONF_ACTION_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_SWING_MODE_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_SWING_MODE_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional( CONF_SWING_MODE_LIST, default=[STATE_ON, HVAC_MODE_OFF] @@ -220,10 +242,13 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_TEMP_MIN, default=DEFAULT_MIN_TEMP): vol.Coerce(float), vol.Optional(CONF_TEMP_MAX, default=DEFAULT_MAX_TEMP): vol.Coerce(float), vol.Optional(CONF_TEMP_STEP, default=1.0): vol.Coerce(float), + vol.Optional(CONF_TEMP_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_TEMP_COMMAND_TOPIC): mqtt.valid_publish_topic, + vol.Optional(CONF_TEMP_HIGH_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_TEMP_HIGH_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_TEMP_HIGH_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_TEMP_HIGH_STATE_TEMPLATE): cv.template, + vol.Optional(CONF_TEMP_LOW_COMMAND_TEMPLATE): cv.template, vol.Optional(CONF_TEMP_LOW_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_TEMP_LOW_STATE_TEMPLATE): cv.template, vol.Optional(CONF_TEMP_LOW_STATE_TOPIC): mqtt.valid_subscribe_topic, @@ -234,13 +259,13 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_VALUE_TEMPLATE): cv.template, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) ) async def async_setup_platform( - hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None + hass: HomeAssistantType, async_add_entities, config: ConfigType, discovery_info=None ): """Set up MQTT climate device through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) @@ -250,46 +275,24 @@ async def async_setup_platform( async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT climate device dynamically through MQTT discovery.""" - async def async_discover(discovery_payload): - """Discover and add a MQTT climate device.""" - discovery_data = discovery_payload.discovery_data - try: - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity( - hass, config, async_add_entities, config_entry, discovery_data - ) - except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(climate.DOMAIN, "mqtt"), async_discover + setup = functools.partial( + _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) + await async_setup_entry_helper(hass, climate.DOMAIN, setup, PLATFORM_SCHEMA) async def _async_setup_entity( - hass, config, async_add_entities, config_entry=None, discovery_data=None + hass, async_add_entities, config, config_entry=None, discovery_data=None ): """Set up the MQTT climate devices.""" async_add_entities([MqttClimate(hass, config, config_entry, discovery_data)]) -class MqttClimate( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - ClimateEntity, -): +class MqttClimate(MqttEntity, ClimateEntity): """Representation of an MQTT climate device.""" def __init__(self, hass, config, config_entry, discovery_data): """Initialize the climate device.""" - self._config = config - self._unique_id = config.get(CONF_UNIQUE_ID) - self._sub_state = None - - self.hass = hass self._action = None self._aux = False self._away = False @@ -303,34 +306,23 @@ class MqttClimate( self._target_temp_low = None self._topic = None self._value_templates = None + self._command_templates = None - self._setup_from_config(config) + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) - device_config = config.get(CONF_DEVICE) - - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA async def async_added_to_hass(self): """Handle being added to Home Assistant.""" await super().async_added_to_hass() await self._subscribe_topics() - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA(discovery_payload) - self._config = config - self._setup_from_config(config) - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() - def _setup_from_config(self, config): """(Re)Setup the entity.""" + self._config = config self._topic = {key: config.get(key) for key in TOPIC_KEYS} # set to None in non-optimistic mode @@ -359,21 +351,30 @@ class MqttClimate( self._aux = False value_templates = {} - for key in TEMPLATE_KEYS: + for key in VALUE_TEMPLATE_KEYS: value_templates[key] = lambda value: value if CONF_VALUE_TEMPLATE in config: value_template = config.get(CONF_VALUE_TEMPLATE) value_template.hass = self.hass value_templates = { key: value_template.async_render_with_possible_json_value - for key in TEMPLATE_KEYS + for key in VALUE_TEMPLATE_KEYS } - for key in TEMPLATE_KEYS & config.keys(): + for key in VALUE_TEMPLATE_KEYS & config.keys(): tpl = config[key] value_templates[key] = tpl.async_render_with_possible_json_value tpl.hass = self.hass self._value_templates = value_templates + command_templates = {} + for key in COMMAND_TEMPLATE_KEYS: + command_templates[key] = lambda value: value + for key in COMMAND_TEMPLATE_KEYS & config.keys(): + tpl = config[key] + command_templates[key] = tpl.async_render_with_possible_json_value + tpl.hass = self.hass + self._command_templates = command_templates + async def _subscribe_topics(self): """(Re)Subscribe to topics.""" topics = {} @@ -563,30 +564,11 @@ class MqttClimate( self.hass, self._sub_state, topics ) - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - - @property - def should_poll(self): - """Return the polling state.""" - return False - @property def name(self): """Return the name of the climate device.""" return self._config[CONF_NAME] - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - @property def temperature_unit(self): """Return the unit of measurement.""" @@ -685,7 +667,7 @@ class MqttClimate( self._config[CONF_RETAIN], ) - def _set_temperature(self, temp, cmnd_topic, state_topic, attr): + def _set_temperature(self, temp, cmnd_topic, cmnd_template, state_topic, attr): if temp is not None: if self._topic[state_topic] is None: # optimistic mode @@ -695,7 +677,8 @@ class MqttClimate( self._config[CONF_SEND_IF_OFF] or self._current_operation != HVAC_MODE_OFF ): - self._publish(cmnd_topic, temp) + payload = self._command_templates[cmnd_template](temp) + self._publish(cmnd_topic, payload) async def async_set_temperature(self, **kwargs): """Set new target temperatures.""" @@ -706,6 +689,7 @@ class MqttClimate( self._set_temperature( kwargs.get(ATTR_TEMPERATURE), CONF_TEMP_COMMAND_TOPIC, + CONF_TEMP_COMMAND_TEMPLATE, CONF_TEMP_STATE_TOPIC, "_target_temp", ) @@ -713,6 +697,7 @@ class MqttClimate( self._set_temperature( kwargs.get(ATTR_TARGET_TEMP_LOW), CONF_TEMP_LOW_COMMAND_TOPIC, + CONF_TEMP_LOW_COMMAND_TEMPLATE, CONF_TEMP_LOW_STATE_TOPIC, "_target_temp_low", ) @@ -720,6 +705,7 @@ class MqttClimate( self._set_temperature( kwargs.get(ATTR_TARGET_TEMP_HIGH), CONF_TEMP_HIGH_COMMAND_TOPIC, + CONF_TEMP_HIGH_COMMAND_TEMPLATE, CONF_TEMP_HIGH_STATE_TOPIC, "_target_temp_high", ) @@ -730,7 +716,10 @@ class MqttClimate( async def async_set_swing_mode(self, swing_mode): """Set new swing mode.""" if self._config[CONF_SEND_IF_OFF] or self._current_operation != HVAC_MODE_OFF: - self._publish(CONF_SWING_MODE_COMMAND_TOPIC, swing_mode) + payload = self._command_templates[CONF_SWING_MODE_COMMAND_TEMPLATE]( + swing_mode + ) + self._publish(CONF_SWING_MODE_COMMAND_TOPIC, payload) if self._topic[CONF_SWING_MODE_STATE_TOPIC] is None: self._current_swing_mode = swing_mode @@ -739,7 +728,8 @@ class MqttClimate( async def async_set_fan_mode(self, fan_mode): """Set new target temperature.""" if self._config[CONF_SEND_IF_OFF] or self._current_operation != HVAC_MODE_OFF: - self._publish(CONF_FAN_MODE_COMMAND_TOPIC, fan_mode) + payload = self._command_templates[CONF_FAN_MODE_COMMAND_TEMPLATE](fan_mode) + self._publish(CONF_FAN_MODE_COMMAND_TOPIC, payload) if self._topic[CONF_FAN_MODE_STATE_TOPIC] is None: self._current_fan_mode = fan_mode @@ -752,7 +742,8 @@ class MqttClimate( elif self._current_operation != HVAC_MODE_OFF and hvac_mode == HVAC_MODE_OFF: self._publish(CONF_POWER_COMMAND_TOPIC, self._config[CONF_PAYLOAD_OFF]) - self._publish(CONF_MODE_COMMAND_TOPIC, hvac_mode) + payload = self._command_templates[CONF_MODE_COMMAND_TEMPLATE](hvac_mode) + self._publish(CONF_MODE_COMMAND_TOPIC, payload) if self._topic[CONF_MODE_STATE_TOPIC] is None: self._current_operation = hvac_mode @@ -812,7 +803,10 @@ class MqttClimate( Returns if we should optimistically write the state. """ - self._publish(CONF_HOLD_COMMAND_TOPIC, hold_mode or "off") + payload = self._command_templates[CONF_HOLD_COMMAND_TEMPLATE]( + hold_mode or "off" + ) + self._publish(CONF_HOLD_COMMAND_TOPIC, payload) if self._topic[CONF_HOLD_STATE_TOPIC] is not None: return False diff --git a/homeassistant/components/mqtt/cover.py b/homeassistant/components/mqtt/cover.py index 25fcf0ad0d2..dc2cba0efab 100644 --- a/homeassistant/components/mqtt/cover.py +++ b/homeassistant/components/mqtt/cover.py @@ -1,4 +1,5 @@ """Support for MQTT cover devices.""" +import functools import logging import voluptuous as vol @@ -33,27 +34,27 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import ( - ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, subscription, ) from .. import mqtt from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, + MqttEntity, + async_setup_entry_helper, +) _LOGGER = logging.getLogger(__name__) @@ -123,7 +124,7 @@ PLATFORM_SCHEMA = vol.All( mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_COMMAND_TOPIC): mqtt.valid_publish_topic, - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_GET_POSITION_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, @@ -164,8 +165,8 @@ PLATFORM_SCHEMA = vol.All( vol.Optional(CONF_VALUE_TEMPLATE): cv.template, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema), + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema), validate_options, ) @@ -175,81 +176,43 @@ async def async_setup_platform( ): """Set up MQTT cover through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, config, async_add_entities) + await _async_setup_entity(hass, async_add_entities, config) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT cover dynamically through MQTT discovery.""" - async def async_discover(discovery_payload): - """Discover and add an MQTT cover.""" - discovery_data = discovery_payload.discovery_data - try: - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity( - hass, config, async_add_entities, config_entry, discovery_data - ) - except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(cover.DOMAIN, "mqtt"), async_discover + setup = functools.partial( + _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) + await async_setup_entry_helper(hass, cover.DOMAIN, setup, PLATFORM_SCHEMA) async def _async_setup_entity( - hass, config, async_add_entities, config_entry=None, discovery_data=None + hass, async_add_entities, config, config_entry=None, discovery_data=None ): """Set up the MQTT Cover.""" async_add_entities([MqttCover(hass, config, config_entry, discovery_data)]) -class MqttCover( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - CoverEntity, -): +class MqttCover(MqttEntity, CoverEntity): """Representation of a cover that can be controlled using MQTT.""" def __init__(self, hass, config, config_entry, discovery_data): """Initialize the cover.""" - self.hass = hass - self._unique_id = config.get(CONF_UNIQUE_ID) self._position = None self._state = None - self._sub_state = None self._optimistic = None self._tilt_value = None self._tilt_optimistic = None - # Load config - self._setup_from_config(config) + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) - device_config = config.get(CONF_DEVICE) - - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) - - async def async_added_to_hass(self): - """Subscribe MQTT events.""" - await super().async_added_to_hass() - await self._subscribe_topics() - - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA(discovery_payload) - self._setup_from_config(config) - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA def _setup_from_config(self, config): self._config = config @@ -374,20 +337,6 @@ class MqttCover( self.hass, self._sub_state, topics ) - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - - @property - def should_poll(self): - """No polling needed.""" - return False - @property def assumed_state(self): """Return true if we do optimistic updates.""" @@ -635,8 +584,3 @@ class MqttCover( if range_type == TILT_PAYLOAD and self._config[CONF_TILT_INVERT_STATE]: position = max_range - position + offset return position - - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id diff --git a/homeassistant/components/mqtt/device_automation.py b/homeassistant/components/mqtt/device_automation.py index c064cca599d..d3e1f33421d 100644 --- a/homeassistant/components/mqtt/device_automation.py +++ b/homeassistant/components/mqtt/device_automation.py @@ -1,14 +1,14 @@ """Provides device automations for MQTT.""" +import functools import logging import voluptuous as vol from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import ATTR_DISCOVERY_HASH, device_trigger +from . import device_trigger from .. import mqtt -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import async_setup_entry_helper _LOGGER = logging.getLogger(__name__) @@ -32,20 +32,14 @@ async def async_setup_entry(hass, config_entry): return await device_trigger.async_device_removed(hass, event.data["device_id"]) - async def async_discover(discovery_payload): - """Discover and add an MQTT device automation.""" - discovery_data = discovery_payload.discovery_data - try: - config = PLATFORM_SCHEMA(discovery_payload) - if config[CONF_AUTOMATION_TYPE] == AUTOMATION_TYPE_TRIGGER: - await device_trigger.async_setup_trigger( - hass, config, config_entry, discovery_data - ) - except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format("device_automation", "mqtt"), async_discover - ) + setup = functools.partial(_async_setup_automation, hass, config_entry=config_entry) + await async_setup_entry_helper(hass, "device_automation", setup, PLATFORM_SCHEMA) hass.bus.async_listen(EVENT_DEVICE_REGISTRY_UPDATED, async_device_removed) + + +async def _async_setup_automation(hass, config, config_entry, discovery_data): + """Set up an MQTT device automation.""" + if config[CONF_AUTOMATION_TYPE] == AUTOMATION_TYPE_TRIGGER: + await device_trigger.async_setup_trigger( + hass, config, config_entry, discovery_data + ) diff --git a/homeassistant/components/mqtt/device_tracker/schema_discovery.py b/homeassistant/components/mqtt/device_tracker/schema_discovery.py index 4de2ae4fa6d..8b51b9fac0e 100644 --- a/homeassistant/components/mqtt/device_tracker/schema_discovery.py +++ b/homeassistant/components/mqtt/device_tracker/schema_discovery.py @@ -1,4 +1,5 @@ """Support for tracking MQTT enabled devices identified through discovery.""" +import functools import logging import voluptuous as vol @@ -20,19 +21,18 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .. import ( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - subscription, -) +from .. import subscription from ... import mqtt -from ..const import ATTR_DISCOVERY_HASH, CONF_QOS, CONF_STATE_TOPIC +from ..const import CONF_QOS, CONF_STATE_TOPIC from ..debug_info import log_messages -from ..discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from ..mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, + MqttEntity, + async_setup_entry_helper, +) _LOGGER = logging.getLogger(__name__) @@ -43,7 +43,7 @@ CONF_SOURCE_TYPE = "source_type" PLATFORM_SCHEMA_DISCOVERY = ( mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_PAYLOAD_HOME, default=STATE_HOME): cv.string, @@ -52,78 +52,42 @@ PLATFORM_SCHEMA_DISCOVERY = ( vol.Optional(CONF_UNIQUE_ID): cv.string, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) ) async def async_setup_entry_from_discovery(hass, config_entry, async_add_entities): """Set up MQTT device tracker dynamically through MQTT discovery.""" - async def async_discover(discovery_payload): - """Discover and add an MQTT device tracker.""" - discovery_data = discovery_payload.discovery_data - try: - config = PLATFORM_SCHEMA_DISCOVERY(discovery_payload) - await _async_setup_entity( - hass, config, async_add_entities, config_entry, discovery_data - ) - except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(device_tracker.DOMAIN, "mqtt"), async_discover + setup = functools.partial( + _async_setup_entity, hass, async_add_entities, config_entry=config_entry + ) + await async_setup_entry_helper( + hass, device_tracker.DOMAIN, setup, PLATFORM_SCHEMA_DISCOVERY ) async def _async_setup_entity( - hass, config, async_add_entities, config_entry=None, discovery_data=None + hass, async_add_entities, config, config_entry=None, discovery_data=None ): """Set up the MQTT Device Tracker entity.""" async_add_entities([MqttDeviceTracker(hass, config, config_entry, discovery_data)]) -class MqttDeviceTracker( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - TrackerEntity, -): +class MqttDeviceTracker(MqttEntity, TrackerEntity): """Representation of a device tracker using MQTT.""" def __init__(self, hass, config, config_entry, discovery_data): """Initialize the tracker.""" - self.hass = hass self._location_name = None - self._sub_state = None - self._unique_id = config.get(CONF_UNIQUE_ID) - # Load config - self._setup_from_config(config) + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) - device_config = config.get(CONF_DEVICE) - - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) - - async def async_added_to_hass(self): - """Subscribe to MQTT events.""" - await super().async_added_to_hass() - await self._subscribe_topics() - - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA_DISCOVERY(discovery_payload) - self._setup_from_config(config) - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA_DISCOVERY def _setup_from_config(self, config): """(Re)Setup the entity.""" @@ -165,15 +129,6 @@ class MqttDeviceTracker( }, ) - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - @property def icon(self): """Return the icon of the device.""" @@ -219,11 +174,6 @@ class MqttDeviceTracker( """Return the name of the device tracker.""" return self._config.get(CONF_NAME) - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - @property def source_type(self): """Return the source type, eg gps or router, of the device.""" diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 9fa51bebf09..6a04fd48049 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -7,28 +7,34 @@ import voluptuous as vol from homeassistant.components.automation import AutomationActionType from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA -from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE +from homeassistant.const import ( + CONF_DEVICE, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_PLATFORM, + CONF_TYPE, +) from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from . import ( - ATTR_DISCOVERY_HASH, - ATTR_DISCOVERY_TOPIC, - CONF_CONNECTIONS, - CONF_DEVICE, - CONF_IDENTIFIERS, - CONF_PAYLOAD, - CONF_QOS, - DOMAIN, - cleanup_device_registry, - debug_info, - trigger as mqtt_trigger, -) +from . import CONF_PAYLOAD, CONF_QOS, DOMAIN, debug_info, trigger as mqtt_trigger from .. import mqtt -from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash +from .const import ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_TOPIC +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_UPDATED, clear_discovery_hash +from .mixins import ( + CONF_CONNECTIONS, + CONF_IDENTIFIERS, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + cleanup_device_registry, + device_info_from_config, + validate_device_has_at_least_one_identifier, +) _LOGGER = logging.getLogger(__name__) @@ -59,13 +65,13 @@ TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( TRIGGER_DISCOVERY_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( { vol.Required(CONF_AUTOMATION_TYPE): str, - vol.Required(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Required(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_PAYLOAD, default=None): vol.Any(None, cv.string), vol.Required(CONF_TYPE): cv.string, vol.Required(CONF_SUBTYPE): cv.string, }, - mqtt.validate_device_has_at_least_one_identifier, + validate_device_has_at_least_one_identifier, ) DEVICE_TRIGGERS = "mqtt_device_triggers" @@ -169,7 +175,7 @@ async def _update_device(hass, config_entry, config): """Update device registry.""" device_registry = await hass.helpers.device_registry.async_get_registry() config_entry_id = config_entry.entry_id - device_info = mqtt.device_info_from_config(config[CONF_DEVICE]) + device_info = device_info_from_config(config[CONF_DEVICE]) if config_entry_id is not None and device_info is not None: device_info["config_entry_id"] = config_entry_id @@ -206,6 +212,7 @@ async def async_setup_trigger(hass, config, config_entry, discovery_data): await _update_device(hass, config_entry, config) device_trigger = hass.data[DEVICE_TRIGGERS][discovery_id] await device_trigger.update_trigger(config, discovery_hash, remove_signal) + async_dispatcher_send(hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None) remove_signal = async_dispatcher_connect( hass, MQTT_DISCOVERY_UPDATED.format(discovery_hash), discovery_update @@ -220,6 +227,7 @@ async def async_setup_trigger(hass, config, config_entry, discovery_data): ) if device is None: + async_dispatcher_send(hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None) return if DEVICE_TRIGGERS not in hass.data: @@ -244,6 +252,8 @@ async def async_setup_trigger(hass, config, config_entry, discovery_data): hass, discovery_hash, discovery_data, device.id ) + async_dispatcher_send(hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None) + async def async_device_removed(hass: HomeAssistant, device_id: str): """Handle the removal of a device.""" diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 5452d15aa30..347166fdb82 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -1,5 +1,6 @@ """Support for MQTT discovery.""" import asyncio +from collections import deque import functools import json import logging @@ -7,7 +8,10 @@ import re import time from homeassistant.const import CONF_DEVICE, CONF_PLATFORM -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import async_get_mqtt @@ -38,6 +42,7 @@ SUPPORTED_COMPONENTS = [ "fan", "light", "lock", + "number", "scene", "sensor", "switch", @@ -46,6 +51,7 @@ SUPPORTED_COMPONENTS = [ ] ALREADY_DISCOVERED = "mqtt_discovered_components" +PENDING_DISCOVERED = "mqtt_pending_components" CONFIG_ENTRY_IS_SETUP = "mqtt_config_entry_is_setup" DATA_CONFIG_ENTRY_LOCK = "mqtt_config_entry_lock" DATA_CONFIG_FLOW_LOCK = "mqtt_discovery_config_flow_lock" @@ -53,6 +59,7 @@ DISCOVERY_UNSUBSCRIBE = "mqtt_discovery_unsubscribe" INTEGRATION_UNSUBSCRIBE = "mqtt_integration_discovery_unsubscribe" MQTT_DISCOVERY_UPDATED = "mqtt_discovery_updated_{}" MQTT_DISCOVERY_NEW = "mqtt_discovery_new_{}_{}" +MQTT_DISCOVERY_DONE = "mqtt_discovery_done_{}" LAST_DISCOVERY = "mqtt_last_discovery" TOPIC_BASE = "~" @@ -78,7 +85,7 @@ async def async_start( """Start MQTT Discovery.""" mqtt_integrations = {} - async def async_entity_message_received(msg): + async def async_discovery_message_received(msg): """Process the received message.""" hass.data[LAST_DISCOVERY] = time.time() payload = msg.payload @@ -141,8 +148,46 @@ async def async_start( payload[CONF_PLATFORM] = "mqtt" - if ALREADY_DISCOVERED not in hass.data: - hass.data[ALREADY_DISCOVERED] = {} + if discovery_hash in hass.data[PENDING_DISCOVERED]: + pending = hass.data[PENDING_DISCOVERED][discovery_hash]["pending"] + pending.appendleft(payload) + _LOGGER.info( + "Component has already been discovered: %s %s, queuing update", + component, + discovery_id, + ) + return + + await async_process_discovery_payload(component, discovery_id, payload) + + async def async_process_discovery_payload(component, discovery_id, payload): + + _LOGGER.debug("Process discovery payload %s", payload) + discovery_hash = (component, discovery_id) + if discovery_hash in hass.data[ALREADY_DISCOVERED] or payload: + + async def discovery_done(_): + pending = hass.data[PENDING_DISCOVERED][discovery_hash]["pending"] + _LOGGER.debug("Pending discovery for %s: %s", discovery_hash, pending) + if not pending: + hass.data[PENDING_DISCOVERED][discovery_hash]["unsub"]() + hass.data[PENDING_DISCOVERED].pop(discovery_hash) + else: + payload = pending.pop() + await async_process_discovery_payload( + component, discovery_id, payload + ) + + if discovery_hash not in hass.data[PENDING_DISCOVERED]: + hass.data[PENDING_DISCOVERED][discovery_hash] = { + "unsub": async_dispatcher_connect( + hass, + MQTT_DISCOVERY_DONE.format(discovery_hash), + discovery_done, + ), + "pending": deque([]), + } + if discovery_hash in hass.data[ALREADY_DISCOVERED]: # Dispatch update _LOGGER.info( @@ -182,14 +227,30 @@ async def async_start( async_dispatcher_send( hass, MQTT_DISCOVERY_NEW.format(component, "mqtt"), payload ) + else: + # Unhandled discovery message + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) hass.data[DATA_CONFIG_ENTRY_LOCK] = asyncio.Lock() hass.data[DATA_CONFIG_FLOW_LOCK] = asyncio.Lock() hass.data[CONFIG_ENTRY_IS_SETUP] = set() - hass.data[DISCOVERY_UNSUBSCRIBE] = await mqtt.async_subscribe( - hass, f"{discovery_topic}/#", async_entity_message_received, 0 + hass.data[ALREADY_DISCOVERED] = {} + hass.data[PENDING_DISCOVERED] = {} + + discovery_topics = [ + f"{discovery_topic}/+/+/config", + f"{discovery_topic}/+/+/+/config", + ] + hass.data[DISCOVERY_UNSUBSCRIBE] = await asyncio.gather( + *( + mqtt.async_subscribe(hass, topic, async_discovery_message_received, 0) + for topic in discovery_topics + ) ) + hass.data[LAST_DISCOVERY] = time.time() mqtt_integrations = await async_get_mqtt(hass) @@ -236,9 +297,10 @@ async def async_start( async def async_stop(hass: HomeAssistantType) -> bool: """Stop MQTT Discovery.""" - if DISCOVERY_UNSUBSCRIBE in hass.data and hass.data[DISCOVERY_UNSUBSCRIBE]: - hass.data[DISCOVERY_UNSUBSCRIBE]() - hass.data[DISCOVERY_UNSUBSCRIBE] = None + if DISCOVERY_UNSUBSCRIBE in hass.data: + for unsub in hass.data[DISCOVERY_UNSUBSCRIBE]: + unsub() + hass.data[DISCOVERY_UNSUBSCRIBE] = [] if INTEGRATION_UNSUBSCRIBE in hass.data: for key, unsub in list(hass.data[INTEGRATION_UNSUBSCRIBE].items()): unsub() diff --git a/homeassistant/components/mqtt/fan.py b/homeassistant/components/mqtt/fan.py index 96d5fe720c3..e5cebd43714 100644 --- a/homeassistant/components/mqtt/fan.py +++ b/homeassistant/components/mqtt/fan.py @@ -1,4 +1,5 @@ """Support for MQTT fans.""" +import functools import logging import voluptuous as vol @@ -25,27 +26,27 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import ( - ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, subscription, ) from .. import mqtt from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, + MqttEntity, + async_setup_entry_helper, +) _LOGGER = logging.getLogger(__name__) @@ -77,7 +78,7 @@ OSCILLATION = "oscillation" PLATFORM_SCHEMA = ( mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_OSCILLATION_COMMAND_TOPIC): mqtt.valid_publish_topic, @@ -106,8 +107,8 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_UNIQUE_ID): cv.string, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) ) @@ -116,54 +117,34 @@ async def async_setup_platform( ): """Set up MQTT fan through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, config, async_add_entities) + await _async_setup_entity(hass, async_add_entities, config) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT fan dynamically through MQTT discovery.""" - async def async_discover(discovery_payload): - """Discover and add a MQTT fan.""" - discovery_data = discovery_payload.discovery_data - try: - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity( - hass, config, async_add_entities, config_entry, discovery_data - ) - except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(fan.DOMAIN, "mqtt"), async_discover + setup = functools.partial( + _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) + await async_setup_entry_helper(hass, fan.DOMAIN, setup, PLATFORM_SCHEMA) async def _async_setup_entity( - hass, config, async_add_entities, config_entry=None, discovery_data=None + hass, async_add_entities, config, config_entry=None, discovery_data=None ): """Set up the MQTT fan.""" async_add_entities([MqttFan(hass, config, config_entry, discovery_data)]) -class MqttFan( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - FanEntity, -): +class MqttFan(MqttEntity, FanEntity): """A MQTT fan component.""" def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT fan.""" - self.hass = hass - self._unique_id = config.get(CONF_UNIQUE_ID) self._state = False self._speed = None self._oscillation = None self._supported_features = 0 - self._sub_state = None self._topic = None self._payload = None @@ -172,30 +153,12 @@ class MqttFan( self._optimistic_oscillation = None self._optimistic_speed = None - # Load config - self._setup_from_config(config) + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) - device_config = config.get(CONF_DEVICE) - - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) - - async def async_added_to_hass(self): - """Subscribe to MQTT events.""" - await super().async_added_to_hass() - await self._subscribe_topics() - - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA(discovery_payload) - self._setup_from_config(config) - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA def _setup_from_config(self, config): """(Re)Setup the entity.""" @@ -319,20 +282,6 @@ class MqttFan( self.hass, self._sub_state, topics ) - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - - @property - def should_poll(self): - """No polling needed for a MQTT fan.""" - return False - @property def assumed_state(self): """Return true if we do optimistic updates.""" @@ -416,7 +365,7 @@ class MqttFan( elif speed == SPEED_OFF: mqtt_payload = self._payload["SPEED_OFF"] else: - mqtt_payload = speed + raise ValueError(f"{speed} is not a valid fan speed") mqtt.async_publish( self.hass, @@ -451,8 +400,3 @@ class MqttFan( if self._optimistic_oscillation: self._oscillation = oscillating self.async_write_ha_state() - - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id diff --git a/homeassistant/components/mqtt/light/__init__.py b/homeassistant/components/mqtt/light/__init__.py index 393cb2fcf13..e780332d093 100644 --- a/homeassistant/components/mqtt/light/__init__.py +++ b/homeassistant/components/mqtt/light/__init__.py @@ -1,15 +1,15 @@ """Support for MQTT lights.""" +import functools import logging import voluptuous as vol from homeassistant.components import light -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from .. import ATTR_DISCOVERY_HASH, DOMAIN, PLATFORMS -from ..discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .. import DOMAIN, PLATFORMS +from ..mixins import async_setup_entry_helper from .schema import CONF_SCHEMA, MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import PLATFORM_SCHEMA_BASIC, async_setup_entity_basic from .schema_json import PLATFORM_SCHEMA_JSON, async_setup_entity_json @@ -38,31 +38,20 @@ async def async_setup_platform( ): """Set up MQTT light through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, config, async_add_entities) + await _async_setup_entity(hass, async_add_entities, config) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT light dynamically through MQTT discovery.""" - async def async_discover(discovery_payload): - """Discover and add a MQTT light.""" - discovery_data = discovery_payload.discovery_data - try: - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity( - hass, config, async_add_entities, config_entry, discovery_data - ) - except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(light.DOMAIN, "mqtt"), async_discover + setup = functools.partial( + _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) + await async_setup_entry_helper(hass, light.DOMAIN, setup, PLATFORM_SCHEMA) async def _async_setup_entity( - hass, config, async_add_entities, config_entry=None, discovery_data=None + hass, async_add_entities, config, config_entry=None, discovery_data=None ): """Set up a MQTT Light.""" setup_entity = { diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 00ad2671391..04f01ea0d3a 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -31,19 +31,15 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.color as color_util -from .. import ( - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_RETAIN, - CONF_STATE_TOPIC, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - subscription, -) +from .. import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, subscription from ... import mqtt from ..debug_info import log_messages +from ..mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, + MqttEntity, +) from .schema import MQTT_LIGHT_SCHEMA_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -114,7 +110,7 @@ PLATFORM_SCHEMA_BASIC = ( vol.Optional(CONF_COLOR_TEMP_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_COLOR_TEMP_STATE_TOPIC): mqtt.valid_subscribe_topic, vol.Optional(CONF_COLOR_TEMP_VALUE_TEMPLATE): cv.template, - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_EFFECT_COMMAND_TOPIC): mqtt.valid_publish_topic, vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_EFFECT_STATE_TOPIC): mqtt.valid_subscribe_topic, @@ -148,8 +144,8 @@ PLATFORM_SCHEMA_BASIC = ( vol.Optional(CONF_XY_VALUE_TEMPLATE): cv.template, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) ) @@ -164,21 +160,12 @@ async def async_setup_entity_basic( async_add_entities([MqttLight(hass, config, config_entry, discovery_data)]) -class MqttLight( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - LightEntity, - RestoreEntity, -): +class MqttLight(MqttEntity, LightEntity, RestoreEntity): """Representation of a MQTT light.""" def __init__(self, hass, config, config_entry, discovery_data): """Initialize MQTT light.""" - self.hass = hass self._state = False - self._sub_state = None self._brightness = None self._hs = None self._color_temp = None @@ -197,32 +184,13 @@ class MqttLight( self._optimistic_hs = False self._optimistic_white_value = False self._optimistic_xy = False - self._unique_id = config.get(CONF_UNIQUE_ID) - # Load config - self._setup_from_config(config) + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) - device_config = config.get(CONF_DEVICE) - - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) - - async def async_added_to_hass(self): - """Subscribe to MQTT events.""" - await super().async_added_to_hass() - await self._subscribe_topics() - - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA_BASIC(discovery_payload) - self._setup_from_config(config) - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA_BASIC def _setup_from_config(self, config): """(Re)Setup the entity.""" @@ -526,15 +494,6 @@ class MqttLight( self.hass, self._sub_state, topics ) - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - @property def brightness(self): """Return the brightness of this light between 0..255.""" @@ -581,21 +540,11 @@ class MqttLight( return white_value return None - @property - def should_poll(self): - """No polling needed for a MQTT light.""" - return False - @property def name(self): """Return the name of the device if any.""" return self._config[CONF_NAME] - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - @property def is_on(self): """Return true if device is on.""" diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index bb10fd52ae7..489b424f4eb 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -42,19 +42,15 @@ from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType import homeassistant.util.color as color_util -from .. import ( - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_RETAIN, - CONF_STATE_TOPIC, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - subscription, -) +from .. import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, subscription from ... import mqtt from ..debug_info import log_messages +from ..mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, + MqttEntity, +) from .schema import MQTT_LIGHT_SCHEMA_SCHEMA from .schema_basic import CONF_BRIGHTNESS_SCALE @@ -93,7 +89,7 @@ PLATFORM_SCHEMA_JSON = ( CONF_BRIGHTNESS_SCALE, default=DEFAULT_BRIGHTNESS_SCALE ): vol.All(vol.Coerce(int), vol.Range(min=1)), vol.Optional(CONF_COLOR_TEMP, default=DEFAULT_COLOR_TEMP): cv.boolean, - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_EFFECT, default=DEFAULT_EFFECT): cv.boolean, vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]), vol.Optional( @@ -118,8 +114,8 @@ PLATFORM_SCHEMA_JSON = ( vol.Optional(CONF_XY, default=DEFAULT_XY): cv.boolean, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) ) @@ -131,20 +127,12 @@ async def async_setup_entity_json( async_add_entities([MqttLightJson(config, config_entry, discovery_data)]) -class MqttLightJson( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - LightEntity, - RestoreEntity, -): +class MqttLightJson(MqttEntity, LightEntity, RestoreEntity): """Representation of a MQTT JSON light.""" def __init__(self, config, config_entry, discovery_data): """Initialize MQTT JSON light.""" self._state = False - self._sub_state = None self._supported_features = 0 self._topic = None @@ -155,32 +143,13 @@ class MqttLightJson( self._hs = None self._white_value = None self._flash_times = None - self._unique_id = config.get(CONF_UNIQUE_ID) - # Load config - self._setup_from_config(config) + MqttEntity.__init__(self, None, config, config_entry, discovery_data) - device_config = config.get(CONF_DEVICE) - - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) - - async def async_added_to_hass(self): - """Subscribe to MQTT events.""" - await super().async_added_to_hass() - await self._subscribe_topics() - - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA_JSON(discovery_payload) - self._setup_from_config(config) - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA_JSON def _setup_from_config(self, config): """(Re)Setup the entity.""" @@ -206,6 +175,43 @@ class MqttLightJson( self._supported_features |= config[CONF_XY] and SUPPORT_COLOR self._supported_features |= config[CONF_HS] and SUPPORT_COLOR + def _parse_color(self, values): + try: + red = int(values["color"]["r"]) + green = int(values["color"]["g"]) + blue = int(values["color"]["b"]) + + return color_util.color_RGB_to_hs(red, green, blue) + except KeyError: + pass + except ValueError: + _LOGGER.warning("Invalid RGB color value received") + return self._hs + + try: + x_color = float(values["color"]["x"]) + y_color = float(values["color"]["y"]) + + return color_util.color_xy_to_hs(x_color, y_color) + except KeyError: + pass + except ValueError: + _LOGGER.warning("Invalid XY color value received") + return self._hs + + try: + hue = float(values["color"]["h"]) + saturation = float(values["color"]["s"]) + + return (hue, saturation) + except KeyError: + pass + except ValueError: + _LOGGER.warning("Invalid HS color value received") + return self._hs + + return self._hs + async def _subscribe_topics(self): """(Re)Subscribe to topics.""" last_state = await self.async_get_last_state() @@ -221,37 +227,11 @@ class MqttLightJson( elif values["state"] == "OFF": self._state = False - if self._supported_features and SUPPORT_COLOR: - try: - red = int(values["color"]["r"]) - green = int(values["color"]["g"]) - blue = int(values["color"]["b"]) - - self._hs = color_util.color_RGB_to_hs(red, green, blue) - except KeyError: - pass - except ValueError: - _LOGGER.warning("Invalid RGB color value received") - - try: - x_color = float(values["color"]["x"]) - y_color = float(values["color"]["y"]) - - self._hs = color_util.color_xy_to_hs(x_color, y_color) - except KeyError: - pass - except ValueError: - _LOGGER.warning("Invalid XY color value received") - - try: - hue = float(values["color"]["h"]) - saturation = float(values["color"]["s"]) - - self._hs = (hue, saturation) - except KeyError: - pass - except ValueError: - _LOGGER.warning("Invalid HS color value received") + if self._supported_features and SUPPORT_COLOR and "color" in values: + if values["color"] is None: + self._hs = None + else: + self._hs = self._parse_color(values) if self._supported_features and SUPPORT_BRIGHTNESS: try: @@ -267,7 +247,10 @@ class MqttLightJson( if self._supported_features and SUPPORT_COLOR_TEMP: try: - self._color_temp = int(values["color_temp"]) + if values["color_temp"] is None: + self._color_temp = None + else: + self._color_temp = int(values["color_temp"]) except KeyError: pass except ValueError: @@ -315,15 +298,6 @@ class MqttLightJson( if last_state.attributes.get(ATTR_WHITE_VALUE): self._white_value = last_state.attributes.get(ATTR_WHITE_VALUE) - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - @property def brightness(self): """Return the brightness of this light between 0..255.""" @@ -364,21 +338,11 @@ class MqttLightJson( """Return the white property.""" return self._white_value - @property - def should_poll(self): - """No polling needed for a MQTT light.""" - return False - @property def name(self): """Return the name of the device if any.""" return self._config[CONF_NAME] - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - @property def is_on(self): """Return true if device is on.""" diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index e6b22da5af0..e696e99552e 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -33,19 +33,15 @@ import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.color as color_util -from .. import ( - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_RETAIN, - CONF_STATE_TOPIC, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - subscription, -) +from .. import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, subscription from ... import mqtt from ..debug_info import log_messages +from ..mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, + MqttEntity, +) from .schema import MQTT_LIGHT_SCHEMA_SCHEMA _LOGGER = logging.getLogger(__name__) @@ -77,7 +73,7 @@ PLATFORM_SCHEMA_TEMPLATE = ( vol.Optional(CONF_COLOR_TEMP_TEMPLATE): cv.template, vol.Required(CONF_COMMAND_OFF_TEMPLATE): cv.template, vol.Required(CONF_COMMAND_ON_TEMPLATE): cv.template, - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_EFFECT_LIST): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_EFFECT_TEMPLATE): cv.template, vol.Optional(CONF_GREEN_TEMPLATE): cv.template, @@ -91,8 +87,8 @@ PLATFORM_SCHEMA_TEMPLATE = ( vol.Optional(CONF_WHITE_VALUE_TEMPLATE): cv.template, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) .extend(MQTT_LIGHT_SCHEMA_SCHEMA.schema) ) @@ -104,20 +100,12 @@ async def async_setup_entity_template( async_add_entities([MqttLightTemplate(config, config_entry, discovery_data)]) -class MqttLightTemplate( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - LightEntity, - RestoreEntity, -): +class MqttLightTemplate(MqttEntity, LightEntity, RestoreEntity): """Representation of a MQTT Template light.""" def __init__(self, config, config_entry, discovery_data): """Initialize a MQTT Template light.""" self._state = False - self._sub_state = None self._topics = None self._templates = None @@ -129,32 +117,13 @@ class MqttLightTemplate( self._white_value = None self._hs = None self._effect = None - self._unique_id = config.get(CONF_UNIQUE_ID) - # Load config - self._setup_from_config(config) + MqttEntity.__init__(self, None, config, config_entry, discovery_data) - device_config = config.get(CONF_DEVICE) - - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) - - async def async_added_to_hass(self): - """Subscribe to MQTT events.""" - await super().async_added_to_hass() - await self._subscribe_topics() - - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA_TEMPLATE(discovery_payload) - self._setup_from_config(config) - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA_TEMPLATE def _setup_from_config(self, config): """(Re)Setup the entity.""" @@ -300,15 +269,6 @@ class MqttLightTemplate( if last_state.attributes.get(ATTR_WHITE_VALUE): self._white_value = last_state.attributes.get(ATTR_WHITE_VALUE) - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - @property def brightness(self): """Return the brightness of this light between 0..255.""" @@ -339,24 +299,11 @@ class MqttLightTemplate( """Return the white property.""" return self._white_value - @property - def should_poll(self): - """Return True if entity has to be polled for state. - - False if entity pushes its state to HA. - """ - return False - @property def name(self): """Return the name of the entity.""" return self._config[CONF_NAME] - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - @property def is_on(self): """Return True if entity is on.""" diff --git a/homeassistant/components/mqtt/lock.py b/homeassistant/components/mqtt/lock.py index 712f2e0e376..b08f8f8bb43 100644 --- a/homeassistant/components/mqtt/lock.py +++ b/homeassistant/components/mqtt/lock.py @@ -1,4 +1,5 @@ """Support for MQTT locks.""" +import functools import logging import voluptuous as vol @@ -14,27 +15,27 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import ( - ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, subscription, ) from .. import mqtt from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, + MqttEntity, + async_setup_entry_helper, +) _LOGGER = logging.getLogger(__name__) @@ -54,7 +55,7 @@ DEFAULT_STATE_UNLOCKED = "UNLOCKED" PLATFORM_SCHEMA = ( mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, vol.Optional(CONF_PAYLOAD_LOCK, default=DEFAULT_PAYLOAD_LOCK): cv.string, @@ -68,8 +69,8 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_UNIQUE_ID): cv.string, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) ) @@ -78,77 +79,39 @@ async def async_setup_platform( ): """Set up MQTT lock panel through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, config, async_add_entities) + await _async_setup_entity(hass, async_add_entities, config) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT lock dynamically through MQTT discovery.""" - async def async_discover(discovery_payload): - """Discover and add an MQTT lock.""" - discovery_data = discovery_payload.discovery_data - try: - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity( - hass, config, async_add_entities, config_entry, discovery_data - ) - except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(lock.DOMAIN, "mqtt"), async_discover + setup = functools.partial( + _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) + await async_setup_entry_helper(hass, lock.DOMAIN, setup, PLATFORM_SCHEMA) async def _async_setup_entity( - hass, config, async_add_entities, config_entry=None, discovery_data=None + hass, async_add_entities, config, config_entry=None, discovery_data=None ): """Set up the MQTT Lock platform.""" async_add_entities([MqttLock(hass, config, config_entry, discovery_data)]) -class MqttLock( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - LockEntity, -): +class MqttLock(MqttEntity, LockEntity): """Representation of a lock that can be toggled using MQTT.""" def __init__(self, hass, config, config_entry, discovery_data): """Initialize the lock.""" - self.hass = hass - self._unique_id = config.get(CONF_UNIQUE_ID) self._state = False - self._sub_state = None self._optimistic = False - # Load config - self._setup_from_config(config) + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) - device_config = config.get(CONF_DEVICE) - - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) - - async def async_added_to_hass(self): - """Subscribe to MQTT events.""" - await super().async_added_to_hass() - await self._subscribe_topics() - - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA(discovery_payload) - self._setup_from_config(config) - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA def _setup_from_config(self, config): """(Re)Setup the entity.""" @@ -194,30 +157,11 @@ class MqttLock( }, ) - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - - @property - def should_poll(self): - """No polling needed.""" - return False - @property def name(self): """Return the name of the lock.""" return self._config[CONF_NAME] - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - @property def is_locked(self): """Return true if lock is locked.""" diff --git a/homeassistant/components/mqtt/mixins.py b/homeassistant/components/mqtt/mixins.py new file mode 100644 index 00000000000..1ab2054b355 --- /dev/null +++ b/homeassistant/components/mqtt/mixins.py @@ -0,0 +1,586 @@ +"""MQTT component mixins and helpers.""" +from abc import abstractmethod +import json +import logging +from typing import Optional + +import voluptuous as vol + +from homeassistant.const import CONF_DEVICE, CONF_NAME, CONF_UNIQUE_ID +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import ConfigType + +from . import CONF_TOPIC, DATA_MQTT, debug_info, publish, subscription +from .const import ( + ATTR_DISCOVERY_HASH, + ATTR_DISCOVERY_PAYLOAD, + ATTR_DISCOVERY_TOPIC, + CONF_QOS, + DEFAULT_PAYLOAD_AVAILABLE, + DEFAULT_PAYLOAD_NOT_AVAILABLE, + DOMAIN, + MQTT_CONNECTED, + MQTT_DISCONNECTED, +) +from .debug_info import log_messages +from .discovery import ( + MQTT_DISCOVERY_DONE, + MQTT_DISCOVERY_NEW, + MQTT_DISCOVERY_UPDATED, + clear_discovery_hash, + set_discovery_hash, +) +from .models import Message +from .subscription import async_subscribe_topics, async_unsubscribe_topics +from .util import valid_subscribe_topic + +_LOGGER = logging.getLogger(__name__) + +AVAILABILITY_ALL = "all" +AVAILABILITY_ANY = "any" +AVAILABILITY_LATEST = "latest" + +AVAILABILITY_MODES = [AVAILABILITY_ALL, AVAILABILITY_ANY, AVAILABILITY_LATEST] + +CONF_AVAILABILITY = "availability" +CONF_AVAILABILITY_MODE = "availability_mode" +CONF_AVAILABILITY_TOPIC = "availability_topic" +CONF_PAYLOAD_AVAILABLE = "payload_available" +CONF_PAYLOAD_NOT_AVAILABLE = "payload_not_available" +CONF_JSON_ATTRS_TOPIC = "json_attributes_topic" +CONF_JSON_ATTRS_TEMPLATE = "json_attributes_template" + +CONF_IDENTIFIERS = "identifiers" +CONF_CONNECTIONS = "connections" +CONF_MANUFACTURER = "manufacturer" +CONF_MODEL = "model" +CONF_SW_VERSION = "sw_version" +CONF_VIA_DEVICE = "via_device" +CONF_DEPRECATED_VIA_HUB = "via_hub" + +MQTT_AVAILABILITY_SINGLE_SCHEMA = vol.Schema( + { + vol.Exclusive(CONF_AVAILABILITY_TOPIC, "availability"): valid_subscribe_topic, + vol.Optional( + CONF_PAYLOAD_AVAILABLE, default=DEFAULT_PAYLOAD_AVAILABLE + ): cv.string, + vol.Optional( + CONF_PAYLOAD_NOT_AVAILABLE, default=DEFAULT_PAYLOAD_NOT_AVAILABLE + ): cv.string, + } +) + +MQTT_AVAILABILITY_LIST_SCHEMA = vol.Schema( + { + vol.Optional(CONF_AVAILABILITY_MODE, default=AVAILABILITY_LATEST): vol.All( + cv.string, vol.In(AVAILABILITY_MODES) + ), + vol.Exclusive(CONF_AVAILABILITY, "availability"): vol.All( + cv.ensure_list, + [ + { + vol.Optional(CONF_TOPIC): valid_subscribe_topic, + vol.Optional( + CONF_PAYLOAD_AVAILABLE, default=DEFAULT_PAYLOAD_AVAILABLE + ): cv.string, + vol.Optional( + CONF_PAYLOAD_NOT_AVAILABLE, + default=DEFAULT_PAYLOAD_NOT_AVAILABLE, + ): cv.string, + } + ], + ), + } +) + +MQTT_AVAILABILITY_SCHEMA = MQTT_AVAILABILITY_SINGLE_SCHEMA.extend( + MQTT_AVAILABILITY_LIST_SCHEMA.schema +) + + +def validate_device_has_at_least_one_identifier(value: ConfigType) -> ConfigType: + """Validate that a device info entry has at least one identifying value.""" + if value.get(CONF_IDENTIFIERS) or value.get(CONF_CONNECTIONS): + return value + raise vol.Invalid( + "Device must have at least one identifying value in " + "'identifiers' and/or 'connections'" + ) + + +MQTT_ENTITY_DEVICE_INFO_SCHEMA = vol.All( + cv.deprecated(CONF_DEPRECATED_VIA_HUB, CONF_VIA_DEVICE), + vol.Schema( + { + vol.Optional(CONF_IDENTIFIERS, default=list): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(CONF_CONNECTIONS, default=list): vol.All( + cv.ensure_list, [vol.All(vol.Length(2), [cv.string])] + ), + vol.Optional(CONF_MANUFACTURER): cv.string, + vol.Optional(CONF_MODEL): cv.string, + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_SW_VERSION): cv.string, + vol.Optional(CONF_VIA_DEVICE): cv.string, + } + ), + validate_device_has_at_least_one_identifier, +) + +MQTT_JSON_ATTRS_SCHEMA = vol.Schema( + { + vol.Optional(CONF_JSON_ATTRS_TOPIC): valid_subscribe_topic, + vol.Optional(CONF_JSON_ATTRS_TEMPLATE): cv.template, + } +) + + +async def async_setup_entry_helper(hass, domain, async_setup, schema): + """Set up entity, automation or tag creation dynamically through MQTT discovery.""" + + async def async_discover(discovery_payload): + """Discover and add an MQTT entity, automation or tag.""" + discovery_data = discovery_payload.discovery_data + try: + config = schema(discovery_payload) + await async_setup(config, discovery_data=discovery_data) + except Exception: + discovery_hash = discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(hass, discovery_hash) + async_dispatcher_send( + hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) + raise + + async_dispatcher_connect( + hass, MQTT_DISCOVERY_NEW.format(domain, "mqtt"), async_discover + ) + + +class MqttAttributes(Entity): + """Mixin used for platforms that support JSON attributes.""" + + def __init__(self, config: dict) -> None: + """Initialize the JSON attributes mixin.""" + self._attributes = None + self._attributes_sub_state = None + self._attributes_config = config + + async def async_added_to_hass(self) -> None: + """Subscribe MQTT events.""" + await super().async_added_to_hass() + await self._attributes_subscribe_topics() + + async def attributes_discovery_update(self, config: dict): + """Handle updated discovery message.""" + self._attributes_config = config + await self._attributes_subscribe_topics() + + async def _attributes_subscribe_topics(self): + """(Re)Subscribe to topics.""" + attr_tpl = self._attributes_config.get(CONF_JSON_ATTRS_TEMPLATE) + if attr_tpl is not None: + attr_tpl.hass = self.hass + + @callback + @log_messages(self.hass, self.entity_id) + def attributes_message_received(msg: Message) -> None: + try: + payload = msg.payload + if attr_tpl is not None: + payload = attr_tpl.async_render_with_possible_json_value(payload) + json_dict = json.loads(payload) + if isinstance(json_dict, dict): + self._attributes = json_dict + self.async_write_ha_state() + else: + _LOGGER.warning("JSON result was not a dictionary") + self._attributes = None + except ValueError: + _LOGGER.warning("Erroneous JSON: %s", payload) + self._attributes = None + + self._attributes_sub_state = await async_subscribe_topics( + self.hass, + self._attributes_sub_state, + { + CONF_JSON_ATTRS_TOPIC: { + "topic": self._attributes_config.get(CONF_JSON_ATTRS_TOPIC), + "msg_callback": attributes_message_received, + "qos": self._attributes_config.get(CONF_QOS), + } + }, + ) + + async def async_will_remove_from_hass(self): + """Unsubscribe when removed.""" + self._attributes_sub_state = await async_unsubscribe_topics( + self.hass, self._attributes_sub_state + ) + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + + +class MqttAvailability(Entity): + """Mixin used for platforms that report availability.""" + + def __init__(self, config: dict) -> None: + """Initialize the availability mixin.""" + self._availability_sub_state = None + self._available = {} + self._available_latest = False + self._availability_setup_from_config(config) + + async def async_added_to_hass(self) -> None: + """Subscribe MQTT events.""" + await super().async_added_to_hass() + await self._availability_subscribe_topics() + self.async_on_remove( + async_dispatcher_connect(self.hass, MQTT_CONNECTED, self.async_mqtt_connect) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, MQTT_DISCONNECTED, self.async_mqtt_connect + ) + ) + + async def availability_discovery_update(self, config: dict): + """Handle updated discovery message.""" + self._availability_setup_from_config(config) + await self._availability_subscribe_topics() + + def _availability_setup_from_config(self, config): + """(Re)Setup.""" + self._avail_topics = {} + if CONF_AVAILABILITY_TOPIC in config: + self._avail_topics[config[CONF_AVAILABILITY_TOPIC]] = { + CONF_PAYLOAD_AVAILABLE: config[CONF_PAYLOAD_AVAILABLE], + CONF_PAYLOAD_NOT_AVAILABLE: config[CONF_PAYLOAD_NOT_AVAILABLE], + } + + if CONF_AVAILABILITY in config: + for avail in config[CONF_AVAILABILITY]: + self._avail_topics[avail[CONF_TOPIC]] = { + CONF_PAYLOAD_AVAILABLE: avail[CONF_PAYLOAD_AVAILABLE], + CONF_PAYLOAD_NOT_AVAILABLE: avail[CONF_PAYLOAD_NOT_AVAILABLE], + } + + self._avail_config = config + + async def _availability_subscribe_topics(self): + """(Re)Subscribe to topics.""" + + @callback + @log_messages(self.hass, self.entity_id) + def availability_message_received(msg: Message) -> None: + """Handle a new received MQTT availability message.""" + topic = msg.topic + if msg.payload == self._avail_topics[topic][CONF_PAYLOAD_AVAILABLE]: + self._available[topic] = True + self._available_latest = True + elif msg.payload == self._avail_topics[topic][CONF_PAYLOAD_NOT_AVAILABLE]: + self._available[topic] = False + self._available_latest = False + + self.async_write_ha_state() + + self._available = {topic: False for topic in self._avail_topics} + topics = { + f"availability_{topic}": { + "topic": topic, + "msg_callback": availability_message_received, + "qos": self._avail_config[CONF_QOS], + } + for topic in self._avail_topics + } + + self._availability_sub_state = await async_subscribe_topics( + self.hass, + self._availability_sub_state, + topics, + ) + + @callback + def async_mqtt_connect(self): + """Update state on connection/disconnection to MQTT broker.""" + if not self.hass.is_stopping: + self.async_write_ha_state() + + async def async_will_remove_from_hass(self): + """Unsubscribe when removed.""" + self._availability_sub_state = await async_unsubscribe_topics( + self.hass, self._availability_sub_state + ) + + @property + def available(self) -> bool: + """Return if the device is available.""" + if not self.hass.data[DATA_MQTT].connected and not self.hass.is_stopping: + return False + if not self._avail_topics: + return True + if self._avail_config[CONF_AVAILABILITY_MODE] == AVAILABILITY_ALL: + return all(self._available.values()) + if self._avail_config[CONF_AVAILABILITY_MODE] == AVAILABILITY_ANY: + return any(self._available.values()) + return self._available_latest + + +async def cleanup_device_registry(hass, device_id): + """Remove device registry entry if there are no remaining entities or triggers.""" + # Local import to avoid circular dependencies + # pylint: disable=import-outside-toplevel + from . import device_trigger, tag + + device_registry = await hass.helpers.device_registry.async_get_registry() + entity_registry = await hass.helpers.entity_registry.async_get_registry() + if ( + device_id + and not hass.helpers.entity_registry.async_entries_for_device( + entity_registry, device_id, include_disabled_entities=True + ) + and not await device_trigger.async_get_triggers(hass, device_id) + and not tag.async_has_tags(hass, device_id) + ): + device_registry.async_remove_device(device_id) + + +class MqttDiscoveryUpdate(Entity): + """Mixin used to handle updated discovery message.""" + + def __init__(self, discovery_data, discovery_update=None) -> None: + """Initialize the discovery update mixin.""" + self._discovery_data = discovery_data + self._discovery_update = discovery_update + self._remove_signal = None + self._removed_from_hass = False + + async def async_added_to_hass(self) -> None: + """Subscribe to discovery updates.""" + await super().async_added_to_hass() + self._removed_from_hass = False + discovery_hash = ( + self._discovery_data[ATTR_DISCOVERY_HASH] if self._discovery_data else None + ) + + async def _async_remove_state_and_registry_entry(self) -> None: + """Remove entity's state and entity registry entry. + + Remove entity from entity registry if it is registered, this also removes the state. + If the entity is not in the entity registry, just remove the state. + """ + entity_registry = ( + await self.hass.helpers.entity_registry.async_get_registry() + ) + if entity_registry.async_is_registered(self.entity_id): + entity_entry = entity_registry.async_get(self.entity_id) + entity_registry.async_remove(self.entity_id) + await cleanup_device_registry(self.hass, entity_entry.device_id) + else: + await self.async_remove() + + async def discovery_callback(payload): + """Handle discovery update.""" + _LOGGER.info( + "Got update for entity with hash: %s '%s'", + discovery_hash, + payload, + ) + old_payload = self._discovery_data[ATTR_DISCOVERY_PAYLOAD] + debug_info.update_entity_discovery_data(self.hass, payload, self.entity_id) + if not payload: + # Empty payload: Remove component + _LOGGER.info("Removing component: %s", self.entity_id) + self._cleanup_discovery_on_remove() + await _async_remove_state_and_registry_entry(self) + elif self._discovery_update: + if old_payload != self._discovery_data[ATTR_DISCOVERY_PAYLOAD]: + # Non-empty, changed payload: Notify component + _LOGGER.info("Updating component: %s", self.entity_id) + await self._discovery_update(payload) + else: + # Non-empty, unchanged payload: Ignore to avoid changing states + _LOGGER.info("Ignoring unchanged update for: %s", self.entity_id) + async_dispatcher_send( + self.hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) + + if discovery_hash: + debug_info.add_entity_discovery_data( + self.hass, self._discovery_data, self.entity_id + ) + # Set in case the entity has been removed and is re-added, for example when changing entity_id + set_discovery_hash(self.hass, discovery_hash) + self._remove_signal = async_dispatcher_connect( + self.hass, + MQTT_DISCOVERY_UPDATED.format(discovery_hash), + discovery_callback, + ) + async_dispatcher_send( + self.hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) + + async def async_removed_from_registry(self) -> None: + """Clear retained discovery topic in broker.""" + if not self._removed_from_hass: + discovery_topic = self._discovery_data[ATTR_DISCOVERY_TOPIC] + publish(self.hass, discovery_topic, "", retain=True) + + @callback + def add_to_platform_abort(self) -> None: + """Abort adding an entity to a platform.""" + if self._discovery_data: + discovery_hash = self._discovery_data[ATTR_DISCOVERY_HASH] + clear_discovery_hash(self.hass, discovery_hash) + async_dispatcher_send( + self.hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) + super().add_to_platform_abort() + + async def async_will_remove_from_hass(self) -> None: + """Stop listening to signal and cleanup discovery data..""" + self._cleanup_discovery_on_remove() + + def _cleanup_discovery_on_remove(self) -> None: + """Stop listening to signal and cleanup discovery data.""" + if self._discovery_data and not self._removed_from_hass: + debug_info.remove_entity_data(self.hass, self.entity_id) + clear_discovery_hash(self.hass, self._discovery_data[ATTR_DISCOVERY_HASH]) + self._removed_from_hass = True + + if self._remove_signal: + self._remove_signal() + self._remove_signal = None + + +def device_info_from_config(config): + """Return a device description for device registry.""" + if not config: + return None + + info = { + "identifiers": {(DOMAIN, id_) for id_ in config[CONF_IDENTIFIERS]}, + "connections": {tuple(x) for x in config[CONF_CONNECTIONS]}, + } + + if CONF_MANUFACTURER in config: + info["manufacturer"] = config[CONF_MANUFACTURER] + + if CONF_MODEL in config: + info["model"] = config[CONF_MODEL] + + if CONF_NAME in config: + info["name"] = config[CONF_NAME] + + if CONF_SW_VERSION in config: + info["sw_version"] = config[CONF_SW_VERSION] + + if CONF_VIA_DEVICE in config: + info["via_device"] = (DOMAIN, config[CONF_VIA_DEVICE]) + + return info + + +class MqttEntityDeviceInfo(Entity): + """Mixin used for mqtt platforms that support the device registry.""" + + def __init__(self, device_config: Optional[ConfigType], config_entry=None) -> None: + """Initialize the device mixin.""" + self._device_config = device_config + self._config_entry = config_entry + + async def device_info_discovery_update(self, config: dict): + """Handle updated discovery message.""" + self._device_config = config.get(CONF_DEVICE) + device_registry = await self.hass.helpers.device_registry.async_get_registry() + config_entry_id = self._config_entry.entry_id + device_info = self.device_info + + if config_entry_id is not None and device_info is not None: + device_info["config_entry_id"] = config_entry_id + device_registry.async_get_or_create(**device_info) + + @property + def device_info(self): + """Return a device description for device registry.""" + return device_info_from_config(self._device_config) + + +class MqttEntity( + MqttAttributes, + MqttAvailability, + MqttDiscoveryUpdate, + MqttEntityDeviceInfo, +): + """Representation of an MQTT entity.""" + + def __init__(self, hass, config, config_entry, discovery_data): + """Init the MQTT Entity.""" + self.hass = hass + self._unique_id = config.get(CONF_UNIQUE_ID) + self._sub_state = None + + # Load config + self._setup_from_config(config) + + # Initialize mixin classes + MqttAttributes.__init__(self, config) + MqttAvailability.__init__(self, config) + MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) + MqttEntityDeviceInfo.__init__(self, config.get(CONF_DEVICE), config_entry) + + async def async_added_to_hass(self): + """Subscribe mqtt events.""" + await super().async_added_to_hass() + await self._subscribe_topics() + + async def discovery_update(self, discovery_payload): + """Handle updated discovery message.""" + config = self.config_schema()(discovery_payload) + self._setup_from_config(config) + await self.attributes_discovery_update(config) + await self.availability_discovery_update(config) + await self.device_info_discovery_update(config) + await self._subscribe_topics() + self.async_write_ha_state() + + async def async_will_remove_from_hass(self): + """Unsubscribe when removed.""" + self._sub_state = await subscription.async_unsubscribe_topics( + self.hass, self._sub_state + ) + await MqttAttributes.async_will_remove_from_hass(self) + await MqttAvailability.async_will_remove_from_hass(self) + await MqttDiscoveryUpdate.async_will_remove_from_hass(self) + + @staticmethod + @abstractmethod + def config_schema(): + """Return the config schema.""" + + def _setup_from_config(self, config): + """(Re)Setup the entity.""" + + @abstractmethod + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def unique_id(self): + """Return a unique ID.""" + return self._unique_id diff --git a/homeassistant/components/mqtt/number.py b/homeassistant/components/mqtt/number.py new file mode 100644 index 00000000000..159f466f7ae --- /dev/null +++ b/homeassistant/components/mqtt/number.py @@ -0,0 +1,182 @@ +"""Configure number in a device through MQTT topic.""" +import functools +import logging + +import voluptuous as vol + +from homeassistant.components import number +from homeassistant.components.number import NumberEntity +from homeassistant.const import ( + CONF_DEVICE, + CONF_ICON, + CONF_NAME, + CONF_OPTIMISTIC, + CONF_UNIQUE_ID, +) +from homeassistant.core import callback +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.reload import async_setup_reload_service +from homeassistant.helpers.restore_state import RestoreEntity +from homeassistant.helpers.typing import ConfigType, HomeAssistantType + +from . import ( + CONF_COMMAND_TOPIC, + CONF_QOS, + CONF_STATE_TOPIC, + DOMAIN, + PLATFORMS, + subscription, +) +from .. import mqtt +from .debug_info import log_messages +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, + MqttEntity, + async_setup_entry_helper, +) + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "MQTT Number" +DEFAULT_OPTIMISTIC = False + +PLATFORM_SCHEMA = ( + mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_ICON): cv.icon, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, + vol.Optional(CONF_UNIQUE_ID): cv.string, + } + ) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) +) + + +async def async_setup_platform( + hass: HomeAssistantType, config: ConfigType, async_add_entities, discovery_info=None +): + """Set up MQTT number through configuration.yaml.""" + await async_setup_reload_service(hass, DOMAIN, PLATFORMS) + await _async_setup_entity(async_add_entities, config) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up MQTT number dynamically through MQTT discovery.""" + + setup = functools.partial( + _async_setup_entity, async_add_entities, config_entry=config_entry + ) + await async_setup_entry_helper(hass, number.DOMAIN, setup, PLATFORM_SCHEMA) + + +async def _async_setup_entity( + async_add_entities, config, config_entry=None, discovery_data=None +): + """Set up the MQTT number.""" + async_add_entities([MqttNumber(config, config_entry, discovery_data)]) + + +class MqttNumber(MqttEntity, NumberEntity, RestoreEntity): + """representation of an MQTT number.""" + + def __init__(self, config, config_entry, discovery_data): + """Initialize the MQTT Number.""" + self._sub_state = None + + self._current_number = None + self._optimistic = config.get(CONF_OPTIMISTIC) + self._unique_id = config.get(CONF_UNIQUE_ID) + + NumberEntity.__init__(self) + MqttEntity.__init__(self, None, config, config_entry, discovery_data) + + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA + + def _setup_from_config(self, config): + self._config = config + + async def _subscribe_topics(self): + """(Re)Subscribe to topics.""" + + @callback + @log_messages(self.hass, self.entity_id) + def message_received(msg): + """Handle new MQTT messages.""" + + try: + if msg.payload.decode("utf-8").isnumeric(): + self._current_number = int(msg.payload) + else: + self._current_number = float(msg.payload) + self.async_write_ha_state() + except ValueError: + _LOGGER.warning("We received <%s> which is not a Number", msg.payload) + + if self._config.get(CONF_STATE_TOPIC) is None: + # Force into optimistic mode. + self._optimistic = True + else: + self._sub_state = await subscription.async_subscribe_topics( + self.hass, + self._sub_state, + { + "state_topic": { + "topic": self._config.get(CONF_STATE_TOPIC), + "msg_callback": message_received, + "qos": self._config[CONF_QOS], + "encoding": None, + } + }, + ) + + if self._optimistic: + last_state = await self.async_get_last_state() + if last_state: + self._current_number = last_state.state + + @property + def value(self): + """Return the current value.""" + return self._current_number + + async def async_set_value(self, value: float) -> None: + """Update the current value.""" + + current_number = value + + if value.is_integer(): + current_number = int(value) + + if self._optimistic: + self._current_number = current_number + self.async_write_ha_state() + + mqtt.async_publish( + self.hass, + self._config[CONF_COMMAND_TOPIC], + current_number, + self._config[CONF_QOS], + ) + + @property + def name(self): + """Return the name of this number.""" + return self._config[CONF_NAME] + + @property + def assumed_state(self): + """Return true if we do optimistic updates.""" + return self._optimistic + + @property + def icon(self): + """Return the icon.""" + return self._config.get(CONF_ICON) diff --git a/homeassistant/components/mqtt/scene.py b/homeassistant/components/mqtt/scene.py index 673eb169b19..908f4bafd30 100644 --- a/homeassistant/components/mqtt/scene.py +++ b/homeassistant/components/mqtt/scene.py @@ -1,4 +1,5 @@ """Support for MQTT scenes.""" +import functools import logging import voluptuous as vol @@ -7,22 +8,17 @@ from homeassistant.components import scene from homeassistant.components.scene import Scene from homeassistant.const import CONF_ICON, CONF_NAME, CONF_PAYLOAD_ON, CONF_UNIQUE_ID import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType -from . import ( - ATTR_DISCOVERY_HASH, - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_RETAIN, - DOMAIN, - PLATFORMS, +from . import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, DOMAIN, PLATFORMS +from .. import mqtt +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, MqttAvailability, MqttDiscoveryUpdate, + async_setup_entry_helper, ) -from .. import mqtt -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -38,7 +34,7 @@ PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, } -).extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) +).extend(MQTT_AVAILABILITY_SCHEMA.schema) async def async_setup_platform( @@ -46,31 +42,20 @@ async def async_setup_platform( ): """Set up MQTT scene through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(config, async_add_entities) + await _async_setup_entity(async_add_entities, config) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT scene dynamically through MQTT discovery.""" - async def async_discover(discovery_payload): - """Discover and add a MQTT scene.""" - discovery_data = discovery_payload.discovery_data - try: - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity( - config, async_add_entities, config_entry, discovery_data - ) - except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(scene.DOMAIN, "mqtt"), async_discover + setup = functools.partial( + _async_setup_entity, async_add_entities, config_entry=config_entry ) + await async_setup_entry_helper(hass, scene.DOMAIN, setup, PLATFORM_SCHEMA) async def _async_setup_entity( - config, async_add_entities, config_entry=None, discovery_data=None + async_add_entities, config, config_entry=None, discovery_data=None ): """Set up the MQTT scene.""" async_add_entities([MqttScene(config, config_entry, discovery_data)]) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 1fda8986ef7..3f79ab1bafe 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -1,5 +1,6 @@ """Support for MQTT sensors.""" from datetime import timedelta +import functools import logging from typing import Optional @@ -19,28 +20,23 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util import dt as dt_util -from . import ( - ATTR_DISCOVERY_HASH, - CONF_QOS, - CONF_STATE_TOPIC, - DOMAIN, - PLATFORMS, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - subscription, -) +from . import CONF_QOS, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, subscription from .. import mqtt from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, + MqttAvailability, + MqttEntity, + async_setup_entry_helper, +) _LOGGER = logging.getLogger(__name__) @@ -51,7 +47,7 @@ DEFAULT_FORCE_UPDATE = False PLATFORM_SCHEMA = ( mqtt.MQTT_RO_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_DEVICE_CLASS): DEVICE_CLASSES_SCHEMA, vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, @@ -61,8 +57,8 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) ) @@ -71,47 +67,31 @@ async def async_setup_platform( ): """Set up MQTT sensors through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, config, async_add_entities) + await _async_setup_entity(hass, async_add_entities, config) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT sensors dynamically through MQTT discovery.""" - async def async_discover_sensor(discovery_payload): - """Discover and add a discovered MQTT sensor.""" - discovery_data = discovery_payload.discovery_data - try: - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity( - hass, config, async_add_entities, config_entry, discovery_data - ) - except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(sensor.DOMAIN, "mqtt"), async_discover_sensor + setup = functools.partial( + _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) + await async_setup_entry_helper(hass, sensor.DOMAIN, setup, PLATFORM_SCHEMA) async def _async_setup_entity( - hass, config: ConfigType, async_add_entities, config_entry=None, discovery_data=None + hass, async_add_entities, config: ConfigType, config_entry=None, discovery_data=None ): """Set up MQTT sensor.""" async_add_entities([MqttSensor(hass, config, config_entry, discovery_data)]) -class MqttSensor( - MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, Entity -): +class MqttSensor(MqttEntity, Entity): """Representation of a sensor that can be updated using MQTT.""" def __init__(self, hass, config, config_entry, discovery_data): """Initialize the sensor.""" - self.hass = hass - self._unique_id = config.get(CONF_UNIQUE_ID) self._state = None - self._sub_state = None self._expiration_trigger = None expire_after = config.get(CONF_EXPIRE_AFTER) @@ -120,30 +100,12 @@ class MqttSensor( else: self._expired = None - # Load config - self._setup_from_config(config) + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) - device_config = config.get(CONF_DEVICE) - - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) - - async def async_added_to_hass(self): - """Subscribe to MQTT events.""" - await super().async_added_to_hass() - await self._subscribe_topics() - - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA(discovery_payload) - self._setup_from_config(config) - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA def _setup_from_config(self, config): """(Re)Setup the entity.""" @@ -198,15 +160,6 @@ class MqttSensor( }, ) - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - @callback def _value_is_expired(self, *_): """Triggered when value is expired.""" @@ -214,11 +167,6 @@ class MqttSensor( self._expired = True self.async_write_ha_state() - @property - def should_poll(self): - """No polling needed.""" - return False - @property def name(self): """Return the name of the sensor.""" @@ -239,11 +187,6 @@ class MqttSensor( """Return the state of the entity.""" return self._state - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - @property def icon(self): """Return the icon.""" diff --git a/homeassistant/components/mqtt/switch.py b/homeassistant/components/mqtt/switch.py index 76019680110..d6d476b680d 100644 --- a/homeassistant/components/mqtt/switch.py +++ b/homeassistant/components/mqtt/switch.py @@ -1,4 +1,5 @@ """Support for MQTT switches.""" +import functools import logging import voluptuous as vol @@ -18,28 +19,28 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.reload import async_setup_reload_service from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import ( - ATTR_DISCOVERY_HASH, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, DOMAIN, PLATFORMS, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, subscription, ) from .. import mqtt from .debug_info import log_messages -from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, + MqttEntity, + async_setup_entry_helper, +) _LOGGER = logging.getLogger(__name__) @@ -53,7 +54,7 @@ CONF_STATE_OFF = "state_off" PLATFORM_SCHEMA = ( mqtt.MQTT_RW_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_ICON): cv.icon, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean, @@ -64,8 +65,8 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_UNIQUE_ID): cv.string, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) ) @@ -74,81 +75,42 @@ async def async_setup_platform( ): """Set up MQTT switch through configuration.yaml.""" await async_setup_reload_service(hass, DOMAIN, PLATFORMS) - await _async_setup_entity(hass, config, async_add_entities) + await _async_setup_entity(hass, async_add_entities, config) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT switch dynamically through MQTT discovery.""" - async def async_discover(discovery_payload): - """Discover and add a MQTT switch.""" - discovery_data = discovery_payload.discovery_data - try: - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity( - hass, config, async_add_entities, config_entry, discovery_data - ) - except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(switch.DOMAIN, "mqtt"), async_discover + setup = functools.partial( + _async_setup_entity, hass, async_add_entities, config_entry=config_entry ) + await async_setup_entry_helper(hass, switch.DOMAIN, setup, PLATFORM_SCHEMA) async def _async_setup_entity( - hass, config, async_add_entities, config_entry=None, discovery_data=None + hass, async_add_entities, config, config_entry=None, discovery_data=None ): """Set up the MQTT switch.""" async_add_entities([MqttSwitch(hass, config, config_entry, discovery_data)]) -class MqttSwitch( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - SwitchEntity, - RestoreEntity, -): +class MqttSwitch(MqttEntity, SwitchEntity, RestoreEntity): """Representation of a switch that can be toggled using MQTT.""" def __init__(self, hass, config, config_entry, discovery_data): """Initialize the MQTT switch.""" - self.hass = hass self._state = False - self._sub_state = None self._state_on = None self._state_off = None self._optimistic = None - self._unique_id = config.get(CONF_UNIQUE_ID) - # Load config - self._setup_from_config(config) + MqttEntity.__init__(self, hass, config, config_entry, discovery_data) - device_config = config.get(CONF_DEVICE) - - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) - - async def async_added_to_hass(self): - """Subscribe to MQTT events.""" - await super().async_added_to_hass() - await self._subscribe_topics() - - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA(discovery_payload) - self._setup_from_config(config) - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA def _setup_from_config(self, config): """(Re)Setup the entity.""" @@ -205,20 +167,6 @@ class MqttSwitch( if last_state: self._state = last_state.state == STATE_ON - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - - @property - def should_poll(self): - """Return the polling state.""" - return False - @property def name(self): """Return the name of the switch.""" @@ -234,11 +182,6 @@ class MqttSwitch( """Return true if we do optimistic updates.""" return self._optimistic - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - @property def icon(self): """Return the icon.""" diff --git a/homeassistant/components/mqtt/tag.py b/homeassistant/components/mqtt/tag.py index 75f3bb50309..b691c5cf8ce 100644 --- a/homeassistant/components/mqtt/tag.py +++ b/homeassistant/components/mqtt/tag.py @@ -1,27 +1,30 @@ """Provides tag scanning for MQTT.""" +import functools import logging import voluptuous as vol -from homeassistant.const import CONF_PLATFORM, CONF_VALUE_TEMPLATE +from homeassistant.const import CONF_DEVICE, CONF_PLATFORM, CONF_VALUE_TEMPLATE import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED -from homeassistant.helpers.dispatcher import async_dispatcher_connect - -from . import ( - ATTR_DISCOVERY_HASH, - ATTR_DISCOVERY_TOPIC, - CONF_CONNECTIONS, - CONF_DEVICE, - CONF_IDENTIFIERS, - CONF_QOS, - CONF_TOPIC, - DOMAIN, - cleanup_device_registry, - subscription, +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, ) + +from . import CONF_QOS, CONF_TOPIC, DOMAIN, subscription from .. import mqtt -from .discovery import MQTT_DISCOVERY_NEW, MQTT_DISCOVERY_UPDATED, clear_discovery_hash +from .const import ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_TOPIC +from .discovery import MQTT_DISCOVERY_DONE, MQTT_DISCOVERY_UPDATED, clear_discovery_hash +from .mixins import ( + CONF_CONNECTIONS, + CONF_IDENTIFIERS, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + async_setup_entry_helper, + cleanup_device_registry, + device_info_from_config, + validate_device_has_at_least_one_identifier, +) from .util import valid_subscribe_topic _LOGGER = logging.getLogger(__name__) @@ -31,31 +34,20 @@ TAGS = "mqtt_tags" PLATFORM_SCHEMA = mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_PLATFORM): "mqtt", vol.Required(CONF_TOPIC): valid_subscribe_topic, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, }, - mqtt.validate_device_has_at_least_one_identifier, + validate_device_has_at_least_one_identifier, ) async def async_setup_entry(hass, config_entry): """Set up MQTT tag scan dynamically through MQTT discovery.""" - async def async_discover(discovery_payload): - """Discover and add MQTT tag scan.""" - discovery_data = discovery_payload.discovery_data - try: - config = PLATFORM_SCHEMA(discovery_payload) - await async_setup_tag(hass, config, config_entry, discovery_data) - except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format("tag", "mqtt"), async_discover - ) + setup = functools.partial(async_setup_tag, hass, config_entry=config_entry) + await async_setup_entry_helper(hass, "tag", setup, PLATFORM_SCHEMA) async def async_setup_tag(hass, config, config_entry, discovery_data): @@ -142,6 +134,10 @@ class MQTTTagScanner: self._setup_from_config(config) await self.subscribe_topics() + async_dispatcher_send( + self.hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) + def _setup_from_config(self, config): self._value_template = lambda value, error_value: value if CONF_VALUE_TEMPLATE in config: @@ -163,6 +159,9 @@ class MQTTTagScanner: MQTT_DISCOVERY_UPDATED.format(discovery_hash), self.discovery_update, ) + async_dispatcher_send( + self.hass, MQTT_DISCOVERY_DONE.format(discovery_hash), None + ) async def subscribe_topics(self): """Subscribe to MQTT topics.""" @@ -217,7 +216,7 @@ async def _update_device(hass, config_entry, config): """Update device registry.""" device_registry = await hass.helpers.device_registry.async_get_registry() config_entry_id = config_entry.entry_id - device_info = mqtt.device_info_from_config(config[CONF_DEVICE]) + device_info = device_info_from_config(config[CONF_DEVICE]) if config_entry_id is not None and device_info is not None: device_info["config_entry_id"] = config_entry_id diff --git a/homeassistant/components/mqtt/translations/cs.json b/homeassistant/components/mqtt/translations/cs.json index 325e8dde098..60c323d9051 100644 --- a/homeassistant/components/mqtt/translations/cs.json +++ b/homeassistant/components/mqtt/translations/cs.json @@ -38,13 +38,13 @@ "turn_on": "Zapnout" }, "trigger_type": { - "button_double_press": "Dvakr\u00e1t stisknuto \"{subtype}\"", + "button_double_press": "\"{subtype}\" stisknuto dvakr\u00e1t", "button_long_release": "Uvoln\u011bno \"{subtype}\" po dlouh\u00e9m stisku", - "button_quadruple_press": "\u010cty\u0159ikr\u00e1t stisknuto \"{subtype}\"", - "button_quintuple_press": "P\u011btkr\u00e1t stisknuto \"{subtype}\"", - "button_short_press": "Stiknuto \"{subtype}\"", + "button_quadruple_press": "\"{subtype}\" stisknuto \u010dty\u0159ikr\u00e1t", + "button_quintuple_press": "\"{subtype}\" stisknuto \u010dty\u0159ikr\u00e1t", + "button_short_press": "\"{subtype}\" stisknuto", "button_short_release": "Uvoln\u011bno \"{subtype}\"", - "button_triple_press": "T\u0159ikr\u00e1t stisknuto \"{subtype}\"" + "button_triple_press": "\"{subtype}\" stisknuto t\u0159ikr\u00e1t" } }, "options": { diff --git a/homeassistant/components/mqtt/translations/de.json b/homeassistant/components/mqtt/translations/de.json index a92886eb0c6..3346abfd53e 100644 --- a/homeassistant/components/mqtt/translations/de.json +++ b/homeassistant/components/mqtt/translations/de.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "single_instance_allowed": "Nur eine einzige Konfiguration von MQTT ist zul\u00e4ssig." + "single_instance_allowed": "Bereits konfiguriert. Es ist nur eine Konfiguration m\u00f6glich." }, "error": { - "cannot_connect": "Es konnte keine Verbindung zum Broker hergestellt werden." + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "broker": { @@ -59,12 +59,15 @@ "password": "Passwort", "port": "Port", "username": "Benutzername" - } + }, + "description": "Bitte gib die Verbindungsinformationen deines MQTT-Brokers ein." }, "options": { "data": { + "discovery": "Erkennung aktivieren", "will_enable": "Letzten Willen aktivieren" - } + }, + "description": "Bitte die MQTT-Einstellungen ausw\u00e4hlen." } } } diff --git a/homeassistant/components/mqtt/translations/no.json b/homeassistant/components/mqtt/translations/no.json index 2a9372b3fb0..12c72603a1f 100644 --- a/homeassistant/components/mqtt/translations/no.json +++ b/homeassistant/components/mqtt/translations/no.json @@ -21,8 +21,8 @@ "data": { "discovery": "Aktiver oppdagelse" }, - "description": "Vil du konfigurere Home Assistant til \u00e5 koble til en MQTT megler som er levert av Hass.io-tillegget {addon}?", - "title": "MQTT megler via Hass.io tillegg" + "description": "Vil du konfigurere Home Assistant til \u00e5 koble til en MQTT megler som er levert av Hass.io-tillegg {addon}?", + "title": "MQTT megler via Hass.io-tillegg" } } }, diff --git a/homeassistant/components/mqtt/translations/pl.json b/homeassistant/components/mqtt/translations/pl.json index ce41d059b24..08b1d2f1974 100644 --- a/homeassistant/components/mqtt/translations/pl.json +++ b/homeassistant/components/mqtt/translations/pl.json @@ -38,14 +38,14 @@ "turn_on": "w\u0142\u0105cznik" }, "trigger_type": { - "button_double_press": "\"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty", - "button_long_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", - "button_long_release": "\"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", - "button_quadruple_press": "\"{subtype}\" zostanie czterokrotnie naci\u015bni\u0119ty", - "button_quintuple_press": "\"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty", - "button_short_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty", - "button_short_release": "\"{subtype}\" zostanie zwolniony", - "button_triple_press": "\"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty" + "button_double_press": "przycisk \"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty", + "button_long_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", + "button_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "button_quadruple_press": "przycisk \"{subtype}\" zostanie czterokrotnie naci\u015bni\u0119ty", + "button_quintuple_press": "przycisk \"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty", + "button_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty", + "button_short_release": "przycisk \"{subtype}\" zostanie zwolniony", + "button_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty" } }, "options": { diff --git a/homeassistant/components/mqtt/translations/ru.json b/homeassistant/components/mqtt/translations/ru.json index 0079481d6f2..7cc7a84b28c 100644 --- a/homeassistant/components/mqtt/translations/ru.json +++ b/homeassistant/components/mqtt/translations/ru.json @@ -21,8 +21,8 @@ "data": { "discovery": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0430\u0432\u0442\u043e\u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432" }, - "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io \"{addon}\")?", - "title": "\u0411\u0440\u043e\u043a\u0435\u0440 MQTT (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io)" + "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0431\u0440\u043e\u043a\u0435\u0440\u0443 MQTT (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant \"{addon}\")?", + "title": "\u0411\u0440\u043e\u043a\u0435\u0440 MQTT (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant)" } } }, diff --git a/homeassistant/components/mqtt/translations/tr.json b/homeassistant/components/mqtt/translations/tr.json index 1b73b94d5a4..86dce2b6ea4 100644 --- a/homeassistant/components/mqtt/translations/tr.json +++ b/homeassistant/components/mqtt/translations/tr.json @@ -1,11 +1,52 @@ { "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, "step": { + "broker": { + "data": { + "password": "Parola", + "port": "Port" + } + }, "hassio_confirm": { "data": { "discovery": "Ke\u015ffetmeyi etkinle\u015ftir" } } } + }, + "device_automation": { + "trigger_subtype": { + "turn_off": "Kapat", + "turn_on": "A\u00e7" + }, + "trigger_type": { + "button_double_press": "\" {subtype} \" \u00e7ift t\u0131kland\u0131", + "button_long_press": "\" {subtype} \" s\u00fcrekli olarak bas\u0131ld\u0131", + "button_quadruple_press": "\" {subtype} \" d\u00f6rt kez t\u0131kland\u0131", + "button_quintuple_press": "\" {subtype} \" be\u015fli t\u0131kland\u0131", + "button_short_press": "\" {subtype} \" bas\u0131ld\u0131", + "button_short_release": "\" {subtype} \" yay\u0131nland\u0131", + "button_triple_press": "\" {subtype} \" \u00fc\u00e7 kez t\u0131kland\u0131" + } + }, + "options": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "broker": { + "data": { + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/translations/uk.json b/homeassistant/components/mqtt/translations/uk.json index 747d190a56d..f871db4aa9d 100644 --- a/homeassistant/components/mqtt/translations/uk.json +++ b/homeassistant/components/mqtt/translations/uk.json @@ -1,26 +1,84 @@ { "config": { "abort": { - "single_instance_allowed": "\u0414\u043e\u0437\u0432\u043e\u043b\u0435\u043d\u043e \u043b\u0438\u0448\u0435 \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e MQTT." + "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." }, "error": { - "cannot_connect": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f \u0434\u043e \u0431\u0440\u043e\u043a\u0435\u0440\u0430." + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" }, "step": { "broker": { "data": { "broker": "\u0411\u0440\u043e\u043a\u0435\u0440", - "discovery": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u043f\u043e\u0448\u0443\u043a", + "discovery": "\u0414\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0438 \u0410\u0432\u0442\u043e\u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "port": "\u041f\u043e\u0440\u0442", "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" }, - "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0432\u0430\u0448\u043e\u0433\u043e \u0431\u0440\u043e\u043a\u0435\u0440\u0430 MQTT." + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0437 \u0432\u0430\u0448\u0438\u043c \u0431\u0440\u043e\u043a\u0435\u0440\u043e\u043c MQTT." }, "hassio_confirm": { "data": { - "discovery": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u043f\u043e\u0448\u0443\u043a" - } + "discovery": "\u0414\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0438 \u0410\u0432\u0442\u043e\u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432" + }, + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u0431\u0440\u043e\u043a\u0435\u0440\u0430 MQTT (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Hass.io \"{addon}\")?", + "title": "\u0411\u0440\u043e\u043a\u0435\u0440 MQTT (\u0434\u043e\u0434\u0430\u0442\u043e\u043a \u0434\u043b\u044f Hass.io)" + } + } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "\u041f\u0435\u0440\u0448\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "button_2": "\u0414\u0440\u0443\u0433\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "button_3": "\u0422\u0440\u0435\u0442\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "button_5": "\u041f'\u044f\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "button_6": "\u0428\u043e\u0441\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "turn_off": "\u0412\u0438\u043c\u043a\u043d\u0443\u0442\u0438", + "turn_on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438" + }, + "trigger_type": { + "button_double_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0438", + "button_long_press": "{subtype} \u0434\u043e\u0432\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430", + "button_long_release": "{subtype} \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0456\u0441\u043b\u044f \u0434\u043e\u0432\u0433\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f", + "button_quadruple_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0447\u043e\u0442\u0438\u0440\u0438 \u0440\u0430\u0437\u0438", + "button_quintuple_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u043f'\u044f\u0442\u044c \u0440\u0430\u0437\u0456\u0432", + "button_short_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430", + "button_short_release": "{subtype} \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0456\u0441\u043b\u044f \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f", + "button_triple_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0438" + } + }, + "options": { + "error": { + "bad_birth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u0442\u043e\u043f\u0456\u043a \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f.", + "bad_will": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u0442\u043e\u043f\u0456\u043a \u043f\u0440\u043e \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "broker": { + "data": { + "broker": "\u0411\u0440\u043e\u043a\u0435\u0440", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0437 \u0432\u0430\u0448\u0438\u043c \u0431\u0440\u043e\u043a\u0435\u0440\u043e\u043c MQTT." + }, + "options": { + "data": { + "birth_enable": "\u0412\u0456\u0434\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u0438 \u0442\u043e\u043f\u0456\u043a \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "birth_payload": "\u0417\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0442\u043e\u043f\u0456\u043a\u0430 \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "birth_qos": "QoS \u0442\u043e\u043f\u0456\u043a\u0430 \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "birth_retain": "\u0417\u0431\u0435\u0440\u0456\u0433\u0430\u0442\u0438 \u0442\u043e\u043f\u0456\u043a \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "birth_topic": "\u0422\u043e\u043f\u0456\u043a \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f (LWT)", + "discovery": "\u0414\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0438 \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f", + "will_enable": "\u0412\u0456\u0434\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u0438 \u0442\u043e\u043f\u0456\u043a \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "will_payload": "\u0417\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0442\u043e\u043f\u0456\u043a\u0430 \u043f\u0440\u043e \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "will_qos": "QoS \u0442\u043e\u043f\u0456\u043a\u0430 \u043f\u0440\u043e \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "will_retain": "\u0417\u0431\u0435\u0440\u0456\u0433\u0430\u0442\u0438 \u0442\u043e\u043f\u0456\u043a \u043f\u0440\u043e \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "will_topic": "\u0422\u043e\u043f\u0456\u043a \u043f\u0440\u043e \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f (LWT)" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0438\u0445 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432 MQTT." } } } diff --git a/homeassistant/components/mqtt/vacuum/__init__.py b/homeassistant/components/mqtt/vacuum/__init__.py index f6265d1b96b..e580e874993 100644 --- a/homeassistant/components/mqtt/vacuum/__init__.py +++ b/homeassistant/components/mqtt/vacuum/__init__.py @@ -1,14 +1,14 @@ """Support for MQTT vacuums.""" +import functools import logging import voluptuous as vol from homeassistant.components.vacuum import DOMAIN -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.reload import async_setup_reload_service -from .. import ATTR_DISCOVERY_HASH, DOMAIN as MQTT_DOMAIN, PLATFORMS -from ..discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash +from .. import DOMAIN as MQTT_DOMAIN, PLATFORMS +from ..mixins import async_setup_entry_helper from .schema import CONF_SCHEMA, LEGACY, MQTT_VACUUM_SCHEMA, STATE from .schema_legacy import PLATFORM_SCHEMA_LEGACY, async_setup_entity_legacy from .schema_state import PLATFORM_SCHEMA_STATE, async_setup_entity_state @@ -30,31 +30,20 @@ PLATFORM_SCHEMA = vol.All( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up MQTT vacuum through configuration.yaml.""" await async_setup_reload_service(hass, MQTT_DOMAIN, PLATFORMS) - await _async_setup_entity(config, async_add_entities) + await _async_setup_entity(async_add_entities, config) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up MQTT vacuum dynamically through MQTT discovery.""" - async def async_discover(discovery_payload): - """Discover and add a MQTT vacuum.""" - discovery_data = discovery_payload.discovery_data - try: - config = PLATFORM_SCHEMA(discovery_payload) - await _async_setup_entity( - config, async_add_entities, config_entry, discovery_data - ) - except Exception: - clear_discovery_hash(hass, discovery_data[ATTR_DISCOVERY_HASH]) - raise - - async_dispatcher_connect( - hass, MQTT_DISCOVERY_NEW.format(DOMAIN, "mqtt"), async_discover + setup = functools.partial( + _async_setup_entity, async_add_entities, config_entry=config_entry ) + await async_setup_entry_helper(hass, DOMAIN, setup, PLATFORM_SCHEMA) async def _async_setup_entity( - config, async_add_entities, config_entry=None, discovery_data=None + async_add_entities, config, config_entry=None, discovery_data=None ): """Set up the MQTT vacuum.""" setup_entity = {LEGACY: async_setup_entity_legacy, STATE: async_setup_entity_state} diff --git a/homeassistant/components/mqtt/vacuum/schema_legacy.py b/homeassistant/components/mqtt/vacuum/schema_legacy.py index 65acc9afc71..e7be64be6ae 100644 --- a/homeassistant/components/mqtt/vacuum/schema_legacy.py +++ b/homeassistant/components/mqtt/vacuum/schema_legacy.py @@ -28,15 +28,15 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.icon import icon_for_battery_level -from .. import ( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - subscription, -) +from .. import subscription from ... import mqtt from ..debug_info import log_messages +from ..mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, + MqttEntity, +) from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services _LOGGER = logging.getLogger(__name__) @@ -120,7 +120,7 @@ PLATFORM_SCHEMA_LEGACY = ( vol.Inclusive(CONF_CHARGING_TOPIC, "charging"): mqtt.valid_publish_topic, vol.Inclusive(CONF_CLEANING_TEMPLATE, "cleaning"): cv.template, vol.Inclusive(CONF_CLEANING_TOPIC, "cleaning"): mqtt.valid_publish_topic, - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Inclusive(CONF_DOCKED_TEMPLATE, "docked"): cv.template, vol.Inclusive(CONF_DOCKED_TOPIC, "docked"): mqtt.valid_publish_topic, vol.Inclusive(CONF_ERROR_TEMPLATE, "error"): cv.template, @@ -160,8 +160,8 @@ PLATFORM_SCHEMA_LEGACY = ( vol.Optional(mqtt.CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) .extend(MQTT_VACUUM_SCHEMA.schema) ) @@ -173,16 +173,10 @@ async def async_setup_entity_legacy( async_add_entities([MqttVacuum(config, config_entry, discovery_data)]) -class MqttVacuum( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - VacuumEntity, -): +class MqttVacuum(MqttEntity, VacuumEntity): """Representation of a MQTT-controlled legacy vacuum.""" - def __init__(self, config, config_entry, discovery_info): + def __init__(self, config, config_entry, discovery_data): """Initialize the vacuum.""" self._cleaning = False self._charging = False @@ -192,18 +186,13 @@ class MqttVacuum( self._battery_level = 0 self._fan_speed = "unknown" self._fan_speed_list = [] - self._sub_state = None - self._unique_id = config.get(CONF_UNIQUE_ID) - # Load config - self._setup_from_config(config) + MqttEntity.__init__(self, None, config, config_entry, discovery_data) - device_config = config.get(CONF_DEVICE) - - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_info, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA_LEGACY def _setup_from_config(self, config): self._name = config[CONF_NAME] @@ -254,30 +243,6 @@ class MqttVacuum( ) } - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA_LEGACY(discovery_payload) - self._setup_from_config(config) - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() - - async def async_added_to_hass(self): - """Subscribe MQTT events.""" - await super().async_added_to_hass() - await self._subscribe_topics() - - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - async def _subscribe_topics(self): """(Re)Subscribe to topics.""" for tpl in self._templates.values(): @@ -381,21 +346,11 @@ class MqttVacuum( """Return the name of the vacuum.""" return self._name - @property - def should_poll(self): - """No polling needed for an MQTT vacuum.""" - return False - @property def is_on(self): """Return true if vacuum is on.""" return self._cleaning - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - @property def status(self): """Return a status string for the vacuum.""" diff --git a/homeassistant/components/mqtt/vacuum/schema_state.py b/homeassistant/components/mqtt/vacuum/schema_state.py index 5a8666e5a2e..c754ba1604a 100644 --- a/homeassistant/components/mqtt/vacuum/schema_state.py +++ b/homeassistant/components/mqtt/vacuum/schema_state.py @@ -32,19 +32,15 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from .. import ( - CONF_COMMAND_TOPIC, - CONF_QOS, - CONF_RETAIN, - CONF_STATE_TOPIC, - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - subscription, -) +from .. import CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN, CONF_STATE_TOPIC, subscription from ... import mqtt from ..debug_info import log_messages +from ..mixins import ( + MQTT_AVAILABILITY_SCHEMA, + MQTT_ENTITY_DEVICE_INFO_SCHEMA, + MQTT_JSON_ATTRS_SCHEMA, + MqttEntity, +) from .schema import MQTT_VACUUM_SCHEMA, services_to_strings, strings_to_services _LOGGER = logging.getLogger(__name__) @@ -120,7 +116,7 @@ DEFAULT_PAYLOAD_PAUSE = "pause" PLATFORM_SCHEMA_STATE = ( mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( { - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_DEVICE): MQTT_ENTITY_DEVICE_INFO_SCHEMA, vol.Optional(CONF_FAN_SPEED_LIST, default=[]): vol.All( cv.ensure_list, [cv.string] ), @@ -148,8 +144,8 @@ PLATFORM_SCHEMA_STATE = ( vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean, } ) - .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) - .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) + .extend(MQTT_AVAILABILITY_SCHEMA.schema) + .extend(MQTT_JSON_ATTRS_SCHEMA.schema) .extend(MQTT_VACUUM_SCHEMA.schema) ) @@ -161,32 +157,21 @@ async def async_setup_entity_state( async_add_entities([MqttStateVacuum(config, config_entry, discovery_data)]) -class MqttStateVacuum( - MqttAttributes, - MqttAvailability, - MqttDiscoveryUpdate, - MqttEntityDeviceInfo, - StateVacuumEntity, -): +class MqttStateVacuum(MqttEntity, StateVacuumEntity): """Representation of a MQTT-controlled state vacuum.""" - def __init__(self, config, config_entry, discovery_info): + def __init__(self, config, config_entry, discovery_data): """Initialize the vacuum.""" self._state = None self._state_attrs = {} self._fan_speed_list = [] - self._sub_state = None - self._unique_id = config.get(CONF_UNIQUE_ID) - # Load config - self._setup_from_config(config) + MqttEntity.__init__(self, None, config, config_entry, discovery_data) - device_config = config.get(CONF_DEVICE) - - MqttAttributes.__init__(self, config) - MqttAvailability.__init__(self, config) - MqttDiscoveryUpdate.__init__(self, discovery_info, self.discovery_update) - MqttEntityDeviceInfo.__init__(self, device_config, config_entry) + @staticmethod + def config_schema(): + """Return the config schema.""" + return PLATFORM_SCHEMA_STATE def _setup_from_config(self, config): self._config = config @@ -212,30 +197,6 @@ class MqttStateVacuum( ) } - async def discovery_update(self, discovery_payload): - """Handle updated discovery message.""" - config = PLATFORM_SCHEMA_STATE(discovery_payload) - self._setup_from_config(config) - await self.attributes_discovery_update(config) - await self.availability_discovery_update(config) - await self.device_info_discovery_update(config) - await self._subscribe_topics() - self.async_write_ha_state() - - async def async_added_to_hass(self): - """Subscribe MQTT events.""" - await super().async_added_to_hass() - await self._subscribe_topics() - - async def async_will_remove_from_hass(self): - """Unsubscribe when removed.""" - self._sub_state = await subscription.async_unsubscribe_topics( - self.hass, self._sub_state - ) - await MqttAttributes.async_will_remove_from_hass(self) - await MqttAvailability.async_will_remove_from_hass(self) - await MqttDiscoveryUpdate.async_will_remove_from_hass(self) - async def _subscribe_topics(self): """(Re)Subscribe to topics.""" topics = {} @@ -271,11 +232,6 @@ class MqttStateVacuum( """Return state of vacuum.""" return self._state - @property - def unique_id(self): - """Return a unique ID.""" - return self._unique_id - @property def fan_speed(self): """Return fan speed of the vacuum.""" diff --git a/homeassistant/components/mychevy/manifest.json b/homeassistant/components/mychevy/manifest.json index c98fd0100d2..5c34290f425 100644 --- a/homeassistant/components/mychevy/manifest.json +++ b/homeassistant/components/mychevy/manifest.json @@ -2,6 +2,6 @@ "domain": "mychevy", "name": "myChevrolet", "documentation": "https://www.home-assistant.io/integrations/mychevy", - "requirements": ["mychevy==2.0.1"], + "requirements": ["mychevy==2.1.1"], "codeowners": [] } diff --git a/homeassistant/components/myq/config_flow.py b/homeassistant/components/myq/config_flow.py index d06f01342a1..352283607d8 100644 --- a/homeassistant/components/myq/config_flow.py +++ b/homeassistant/components/myq/config_flow.py @@ -81,12 +81,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(properties["id"]) return await self.async_step_user() - async def async_step_import(self, user_input): - """Handle import.""" - await self.async_set_unique_id(user_input[CONF_USERNAME]) - self._abort_if_unique_id_configured() - return await self.async_step_user(user_input) - class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/myq/cover.py b/homeassistant/components/myq/cover.py index 59a7fb5bb2e..6fef6b25bab 100644 --- a/homeassistant/components/myq/cover.py +++ b/homeassistant/components/myq/cover.py @@ -9,27 +9,16 @@ from pymyq.const import ( KNOWN_MODELS, MANUFACTURER, ) -import voluptuous as vol from homeassistant.components.cover import ( DEVICE_CLASS_GARAGE, DEVICE_CLASS_GATE, - PLATFORM_SCHEMA, SUPPORT_CLOSE, SUPPORT_OPEN, CoverEntity, ) -from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import ( - CONF_PASSWORD, - CONF_TYPE, - CONF_USERNAME, - STATE_CLOSED, - STATE_CLOSING, - STATE_OPENING, -) +from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPENING from homeassistant.core import callback -from homeassistant.helpers import config_validation as cv from homeassistant.helpers.event import async_call_later from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -42,31 +31,6 @@ from .const import ( TRANSITION_START_DURATION, ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - # This parameter is no longer used; keeping it to avoid a breaking change in - # a hotfix, but in a future main release, this should be removed: - vol.Optional(CONF_TYPE): cv.string, - }, -) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the platform.""" - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_USERNAME: config[CONF_USERNAME], - CONF_PASSWORD: config[CONF_PASSWORD], - }, - ) - ) - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up mysq covers.""" diff --git a/homeassistant/components/myq/translations/de.json b/homeassistant/components/myq/translations/de.json index d5c890e4169..fafa38c7817 100644 --- a/homeassistant/components/myq/translations/de.json +++ b/homeassistant/components/myq/translations/de.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "MyQ ist bereits konfiguriert" + "already_configured": "Der Dienst ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/myq/translations/tr.json b/homeassistant/components/myq/translations/tr.json new file mode 100644 index 00000000000..7347d18bc34 --- /dev/null +++ b/homeassistant/components/myq/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "title": "MyQ A\u011f Ge\u00e7idine ba\u011flan\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/translations/uk.json b/homeassistant/components/myq/translations/uk.json new file mode 100644 index 00000000000..12f8406de12 --- /dev/null +++ b/homeassistant/components/myq/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "MyQ" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/de.json b/homeassistant/components/neato/translations/de.json index 94fcd3c4cb2..4c2fc456873 100644 --- a/homeassistant/components/neato/translations/de.json +++ b/homeassistant/components/neato/translations/de.json @@ -4,7 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", "invalid_auth": "Ung\u00fcltige Authentifizierung", - "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte beachte die Dokumentation.", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler sind [im Hilfebereich]({docs_url}) zu finden", "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, @@ -20,7 +20,7 @@ "title": "W\u00e4hle die Authentifizierungsmethode" }, "reauth_confirm": { - "title": "Wollen Sie mit der Einrichtung beginnen?" + "title": "M\u00f6chtest du mit der Einrichtung beginnen?" }, "user": { "data": { diff --git a/homeassistant/components/neato/translations/it.json b/homeassistant/components/neato/translations/it.json index 100237c33e6..95866e918c6 100644 --- a/homeassistant/components/neato/translations/it.json +++ b/homeassistant/components/neato/translations/it.json @@ -2,14 +2,14 @@ "config": { "abort": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", - "authorize_url_timeout": "Timeout nella generazione dell'URL di autorizzazione.", + "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", "invalid_auth": "Autenticazione non valida", - "missing_configuration": "Questo componente non \u00e8 configurato. Per favore segui la documentazione.", - "no_url_available": "Nessun URL disponibile. Per altre informazioni su questo errore, [controlla la sezione di aiuto]({docs_url})", - "reauth_successful": "Ri-autenticazione completata con successo" + "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", + "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", + "reauth_successful": "La riautenticazione ha avuto successo" }, "create_entry": { - "default": "Autenticato con successo" + "default": "Autenticazione riuscita" }, "error": { "invalid_auth": "Autenticazione non valida", @@ -17,10 +17,10 @@ }, "step": { "pick_implementation": { - "title": "Scegli un metodo di autenticazione" + "title": "Scegli il metodo di autenticazione" }, "reauth_confirm": { - "title": "Vuoi cominciare la configurazione?" + "title": "Vuoi iniziare la configurazione?" }, "user": { "data": { diff --git a/homeassistant/components/neato/translations/lb.json b/homeassistant/components/neato/translations/lb.json index 44d8e4f6811..adc42ae840d 100644 --- a/homeassistant/components/neato/translations/lb.json +++ b/homeassistant/components/neato/translations/lb.json @@ -2,7 +2,10 @@ "config": { "abort": { "already_configured": "Apparat ass scho konfigur\u00e9iert", - "invalid_auth": "Ong\u00eblteg Authentifikatioun" + "authorize_url_timeout": "Z\u00e4itiwwerschreidung beim erstellen vun der Authorisatiouns URL.", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "missing_configuration": "Komponent net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun.", + "reauth_successful": "Re-authentifikatioun war erfollegr\u00e4ich" }, "create_entry": { "default": "Kuckt [Neato Dokumentatioun]({docs_url})." @@ -12,6 +15,12 @@ "unknown": "Onerwaarte Feeler" }, "step": { + "pick_implementation": { + "title": "Authentifikatiouns Method auswielen" + }, + "reauth_confirm": { + "title": "Soll den Ariichtungs Prozess gestart ginn?" + }, "user": { "data": { "password": "Passwuert", @@ -22,5 +31,6 @@ "title": "Neato Kont Informatiounen" } } - } + }, + "title": "Neato Botvac" } \ No newline at end of file diff --git a/homeassistant/components/neato/translations/pt.json b/homeassistant/components/neato/translations/pt.json index 0672c9af33f..48e73c763f0 100644 --- a/homeassistant/components/neato/translations/pt.json +++ b/homeassistant/components/neato/translations/pt.json @@ -2,13 +2,26 @@ "config": { "abort": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", - "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida" + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", + "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", + "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", + "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", + "reauth_successful": "Reautentica\u00e7\u00e3o bem sucedida" + }, + "create_entry": { + "default": "Autenticado com sucesso" }, "error": { "invalid_auth": "Autentica\u00e7\u00e3o inv\u00e1lida", "unknown": "Erro inesperado" }, "step": { + "pick_implementation": { + "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" + }, + "reauth_confirm": { + "title": "Quer dar inicio \u00e0 configura\u00e7\u00e3o?" + }, "user": { "data": { "password": "Palavra-passe", diff --git a/homeassistant/components/neato/translations/tr.json b/homeassistant/components/neato/translations/tr.json new file mode 100644 index 00000000000..53a8e0503cb --- /dev/null +++ b/homeassistant/components/neato/translations/tr.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "pick_implementation": { + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7in" + }, + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "title": "Neato Hesap Bilgisi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/uk.json b/homeassistant/components/neato/translations/uk.json new file mode 100644 index 00000000000..58b56a52f6c --- /dev/null +++ b/homeassistant/components/neato/translations/uk.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" + }, + "create_entry": { + "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "pick_implementation": { + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + }, + "reauth_confirm": { + "title": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", + "vendor": "\u0412\u0438\u0440\u043e\u0431\u043d\u0438\u043a" + }, + "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u0440\u043e\u0437\u0448\u0438\u0440\u0435\u043d\u0438\u0445 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u044c.", + "title": "Neato" + } + } + }, + "title": "Neato Botvac" +} \ No newline at end of file diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index 1240d30f027..85e591707ad 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -30,14 +30,7 @@ from homeassistant.helpers import ( ) from . import api, config_flow -from .const import ( - API_URL, - DATA_SDM, - DATA_SUBSCRIBER, - DOMAIN, - OAUTH2_AUTHORIZE, - OAUTH2_TOKEN, -) +from .const import DATA_SDM, DATA_SUBSCRIBER, DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN from .events import EVENT_NAME_MAP, NEST_EVENT from .legacy import async_setup_legacy, async_setup_legacy_entry @@ -128,7 +121,7 @@ class SignalUpdateCallback: return _LOGGER.debug("Event Update %s", events.keys()) device_registry = await self._hass.helpers.device_registry.async_get_registry() - device_entry = device_registry.async_get_device({(DOMAIN, device_id)}, ()) + device_entry = device_registry.async_get_device({(DOMAIN, device_id)}) if not device_entry: return for event in events: @@ -161,7 +154,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): auth = api.AsyncConfigEntryAuth( aiohttp_client.async_get_clientsession(hass), session, - API_URL, + config[CONF_CLIENT_ID], + config[CONF_CLIENT_SECRET], ) subscriber = GoogleNestSubscriber( auth, config[CONF_PROJECT_ID], config[CONF_SUBSCRIBER_ID] diff --git a/homeassistant/components/nest/api.py b/homeassistant/components/nest/api.py index 46138469d6d..3b571354c0f 100644 --- a/homeassistant/components/nest/api.py +++ b/homeassistant/components/nest/api.py @@ -1,11 +1,15 @@ """API for Google Nest Device Access bound to Home Assistant OAuth.""" +import datetime + from aiohttp import ClientSession from google.oauth2.credentials import Credentials from google_nest_sdm.auth import AbstractAuth from homeassistant.helpers import config_entry_oauth2_flow +from .const import API_URL, OAUTH2_TOKEN, SDM_SCOPES + # See https://developers.google.com/nest/device-access/registration @@ -16,20 +20,37 @@ class AsyncConfigEntryAuth(AbstractAuth): self, websession: ClientSession, oauth_session: config_entry_oauth2_flow.OAuth2Session, - api_url: str, + client_id: str, + client_secret: str, ): """Initialize Google Nest Device Access auth.""" - super().__init__(websession, api_url) + super().__init__(websession, API_URL) self._oauth_session = oauth_session + self._client_id = client_id + self._client_secret = client_secret async def async_get_access_token(self): - """Return a valid access token.""" + """Return a valid access token for SDM API.""" if not self._oauth_session.valid_token: await self._oauth_session.async_ensure_token_valid() - return self._oauth_session.token["access_token"] async def async_get_creds(self): - """Return a minimal OAuth credential.""" - token = await self.async_get_access_token() - return Credentials(token=token) + """Return an OAuth credential for Pub/Sub Subscriber.""" + # We don't have a way for Home Assistant to refresh creds on behalf + # of the google pub/sub subscriber. Instead, build a full + # Credentials object with enough information for the subscriber to + # handle this on its own. We purposely don't refresh the token here + # even when it is expired to fully hand off this responsibility and + # know it is working at startup (then if not, fail loudly). + token = self._oauth_session.token + creds = Credentials( + token=token["access_token"], + refresh_token=token["refresh_token"], + token_uri=OAUTH2_TOKEN, + client_id=self._client_id, + client_secret=self._client_secret, + scopes=SDM_SCOPES, + ) + creds.expiry = datetime.datetime.fromtimestamp(token["expires_at"]) + return creds diff --git a/homeassistant/components/nest/camera_sdm.py b/homeassistant/components/nest/camera_sdm.py index a643de0e6c9..aa8e100059a 100644 --- a/homeassistant/components/nest/camera_sdm.py +++ b/homeassistant/components/nest/camera_sdm.py @@ -4,7 +4,11 @@ import datetime import logging from typing import Optional -from google_nest_sdm.camera_traits import CameraImageTrait, CameraLiveStreamTrait +from google_nest_sdm.camera_traits import ( + CameraEventImageTrait, + CameraImageTrait, + CameraLiveStreamTrait, +) from google_nest_sdm.device import Device from google_nest_sdm.exceptions import GoogleNestException from haffmpeg.tools import IMAGE_JPEG @@ -59,6 +63,10 @@ class NestCamera(Camera): self._device_info = DeviceInfo(device) self._stream = None self._stream_refresh_unsub = None + # Cache of most recent event image + self._event_id = None + self._event_image_bytes = None + self._event_image_cleanup_unsub = None @property def should_poll(self) -> bool: @@ -147,6 +155,10 @@ class NestCamera(Camera): await self._stream.stop_rtsp_stream() if self._stream_refresh_unsub: self._stream_refresh_unsub() + self._event_id = None + self._event_image_bytes = None + if self._event_image_cleanup_unsub is not None: + self._event_image_cleanup_unsub() async def async_added_to_hass(self): """Run when entity is added to register update signal handler.""" @@ -156,7 +168,63 @@ class NestCamera(Camera): async def async_camera_image(self): """Return bytes of camera image.""" + # Returns the snapshot of the last event for ~30 seconds after the event + active_event_image = await self._async_active_event_image() + if active_event_image: + return active_event_image + # Fetch still image from the live stream stream_url = await self.stream_source() if not stream_url: return None return await async_get_image(self.hass, stream_url, output_format=IMAGE_JPEG) + + async def _async_active_event_image(self): + """Return image from any active events happening.""" + if CameraEventImageTrait.NAME not in self._device.traits: + return None + trait = self._device.active_event_trait + if not trait: + return None + # Reuse image bytes if they have already been fetched + event = trait.last_event + if self._event_id is not None and self._event_id == event.event_id: + return self._event_image_bytes + _LOGGER.debug("Generating event image URL for event_id %s", event.event_id) + image_bytes = await self._async_fetch_active_event_image(trait) + if image_bytes is None: + return None + self._event_id = event.event_id + self._event_image_bytes = image_bytes + self._schedule_event_image_cleanup(event.expires_at) + return image_bytes + + async def _async_fetch_active_event_image(self, trait): + """Return image bytes for an active event.""" + try: + event_image = await trait.generate_active_event_image() + except GoogleNestException as err: + _LOGGER.debug("Unable to generate event image URL: %s", err) + return None + if not event_image: + return None + try: + return await event_image.contents() + except GoogleNestException as err: + _LOGGER.debug("Unable to fetch event image: %s", err) + return None + + def _schedule_event_image_cleanup(self, point_in_time): + """Schedules an alarm to remove the image bytes from memory, honoring expiration.""" + if self._event_image_cleanup_unsub is not None: + self._event_image_cleanup_unsub() + self._event_image_cleanup_unsub = async_track_point_in_utc_time( + self.hass, + self._handle_event_image_cleanup, + point_in_time, + ) + + def _handle_event_image_cleanup(self, now): + """Clear images cached from events and scheduled callback.""" + self._event_id = None + self._event_image_bytes = None + self._event_image_cleanup_unsub = None diff --git a/homeassistant/components/nest/climate_sdm.py b/homeassistant/components/nest/climate_sdm.py index 08cb0161bd9..6413b2e0dfe 100644 --- a/homeassistant/components/nest/climate_sdm.py +++ b/homeassistant/components/nest/climate_sdm.py @@ -23,6 +23,7 @@ from homeassistant.components.climate.const import ( FAN_ON, HVAC_MODE_AUTO, HVAC_MODE_COOL, + HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, @@ -188,11 +189,14 @@ class ThermostatEntity(ClimateEntity): @property def hvac_mode(self): """Return the current operation (e.g. heat, cool, idle).""" + hvac_mode = HVAC_MODE_OFF if ThermostatModeTrait.NAME in self._device.traits: trait = self._device.traits[ThermostatModeTrait.NAME] if trait.mode in THERMOSTAT_MODE_MAP: - return THERMOSTAT_MODE_MAP[trait.mode] - return HVAC_MODE_OFF + hvac_mode = THERMOSTAT_MODE_MAP[trait.mode] + if hvac_mode == HVAC_MODE_OFF and self.fan_mode == FAN_ON: + hvac_mode = HVAC_MODE_FAN_ONLY + return hvac_mode @property def hvac_modes(self): @@ -201,6 +205,8 @@ class ThermostatEntity(ClimateEntity): for mode in self._get_device_hvac_modes: if mode in THERMOSTAT_MODE_MAP: supported_modes.append(THERMOSTAT_MODE_MAP[mode]) + if self.supported_features & SUPPORT_FAN_MODE: + supported_modes.append(HVAC_MODE_FAN_ONLY) return supported_modes @property @@ -280,6 +286,10 @@ class ThermostatEntity(ClimateEntity): """Set new target hvac mode.""" if hvac_mode not in self.hvac_modes: raise ValueError(f"Unsupported hvac_mode '{hvac_mode}'") + if hvac_mode == HVAC_MODE_FAN_ONLY: + # Turn the fan on but also turn off the hvac if it is on + await self.async_set_fan_mode(FAN_ON) + hvac_mode = HVAC_MODE_OFF api_mode = THERMOSTAT_INV_MODE_MAP[hvac_mode] trait = self._device.traits[ThermostatModeTrait.NAME] await trait.set_mode(api_mode) diff --git a/homeassistant/components/nest/manifest.json b/homeassistant/components/nest/manifest.json index 7d60bb1cf5d..f9bc135693d 100644 --- a/homeassistant/components/nest/manifest.json +++ b/homeassistant/components/nest/manifest.json @@ -4,12 +4,8 @@ "config_flow": true, "dependencies": ["ffmpeg", "http"], "documentation": "https://www.home-assistant.io/integrations/nest", - "requirements": [ - "python-nest==4.1.0", - "google-nest-sdm==0.2.5" - ], - "codeowners": [ - "@awarecan", - "@allenporter" - ] + "requirements": ["python-nest==4.1.0", "google-nest-sdm==0.2.9"], + "codeowners": ["@allenporter"], + "quality_scale": "platinum", + "dhcp": [{"macaddress":"18B430*"}] } diff --git a/homeassistant/components/nest/translations/de.json b/homeassistant/components/nest/translations/de.json index 2bc328ff8f6..3925b7537b2 100644 --- a/homeassistant/components/nest/translations/de.json +++ b/homeassistant/components/nest/translations/de.json @@ -2,34 +2,43 @@ "config": { "abort": { "authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL", - "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL", - "reauth_successful": "Neuathentifizierung erfolgreich", + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", "unknown_authorize_url_generation": "Beim Generieren einer Authentifizierungs-URL ist ein unbekannter Fehler aufgetreten" }, + "create_entry": { + "default": "Erfolgreich authentifiziert" + }, "error": { "internal_error": "Ein interner Fehler ist aufgetreten", "invalid_pin": "Ung\u00fcltiger PIN-Code", "timeout": "Ein zeit\u00fcberschreitungs Fehler ist aufgetreten", - "unknown": "Ein unbekannter Fehler ist aufgetreten" + "unknown": "Unerwarteter Fehler" }, "step": { "init": { "data": { "flow_impl": "Anbieter" }, - "description": "W\u00e4hlen, \u00fcber welchen Authentifizierungsanbieter du dich bei Nest authentifizieren m\u00f6chtest.", + "description": "W\u00e4hle die Authentifizierungsmethode", "title": "Authentifizierungsanbieter" }, "link": { "data": { - "code": "PIN Code" + "code": "PIN-Code" }, "description": "[Autorisiere dein Konto] ( {url} ), um deinen Nest-Account zu verkn\u00fcpfen.\n\n F\u00fcge anschlie\u00dfend den erhaltenen PIN Code hier ein.", "title": "Nest-Konto verkn\u00fcpfen" }, + "pick_implementation": { + "title": "W\u00e4hle die Authentifizierungsmethode" + }, "reauth_confirm": { "description": "Die Nest-Integration muss das Konto neu authentifizieren", - "title": "Integration neu authentifizieren" + "title": "Integration erneut authentifizieren" } } }, diff --git a/homeassistant/components/nest/translations/fr.json b/homeassistant/components/nest/translations/fr.json index be006913f65..03b55458e9b 100644 --- a/homeassistant/components/nest/translations/fr.json +++ b/homeassistant/components/nest/translations/fr.json @@ -5,7 +5,8 @@ "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", "missing_configuration": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", - "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible." + "single_instance_allowed": "D\u00e9j\u00e0 configur\u00e9. Une seule configuration possible.", + "unknown_authorize_url_generation": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation." }, "create_entry": { "default": "Authentification r\u00e9ussie" diff --git a/homeassistant/components/nest/translations/it.json b/homeassistant/components/nest/translations/it.json index 958eaea039a..376437d20f0 100644 --- a/homeassistant/components/nest/translations/it.json +++ b/homeassistant/components/nest/translations/it.json @@ -5,7 +5,7 @@ "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione.", "no_url_available": "Nessun URL disponibile. Per informazioni su questo errore, [controlla la sezione della guida]({docs_url})", - "reauth_successful": "Riautenticato con successo", + "reauth_successful": "La riautenticazione ha avuto successo", "single_instance_allowed": "Gi\u00e0 configurato. \u00c8 possibile una sola configurazione.", "unknown_authorize_url_generation": "Errore sconosciuto durante la generazione di un URL di autorizzazione." }, @@ -38,7 +38,7 @@ }, "reauth_confirm": { "description": "L'integrazione di Nest deve autenticare nuovamente il tuo account", - "title": "Autentica nuovamente l'integrazione" + "title": "Reautenticare l'integrazione" } } }, diff --git a/homeassistant/components/nest/translations/lb.json b/homeassistant/components/nest/translations/lb.json index 1f0115a429b..612d1f30258 100644 --- a/homeassistant/components/nest/translations/lb.json +++ b/homeassistant/components/nest/translations/lb.json @@ -4,7 +4,12 @@ "authorize_url_fail": "Onbekannte Feeler beim gener\u00e9ieren vun der Autorisatiouns URL.", "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", "missing_configuration": "Komponent net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun.", - "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech." + "reauth_successful": "Re-authentifikatioun war erfollegr\u00e4ich", + "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech.", + "unknown_authorize_url_generation": "Onbekannte Feeler beim erstellen vun der Authorisatiouns URL." + }, + "create_entry": { + "default": "Erfollegr\u00e4ich authentifiz\u00e9iert" }, "error": { "internal_error": "Interne Feeler beim valid\u00e9ieren vum Code", diff --git a/homeassistant/components/nest/translations/pl.json b/homeassistant/components/nest/translations/pl.json index 63e45df12fa..d1147e03afc 100644 --- a/homeassistant/components/nest/translations/pl.json +++ b/homeassistant/components/nest/translations/pl.json @@ -5,6 +5,7 @@ "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105.", "no_url_available": "Brak dost\u0119pnego adresu URL. Aby uzyska\u0107 informacje na temat tego b\u0142\u0119du, [sprawd\u017a sekcj\u0119 pomocy] ({docs_url})", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.", "unknown_authorize_url_generation": "Nieznany b\u0142\u0105d podczas generowania URL autoryzacji" }, @@ -34,6 +35,10 @@ }, "pick_implementation": { "title": "Wybierz metod\u0119 uwierzytelniania" + }, + "reauth_confirm": { + "description": "Integracja Nest wymaga ponownego uwierzytelnienia Twojego konta", + "title": "Ponownie uwierzytelnij integracj\u0119" } } }, diff --git a/homeassistant/components/nest/translations/pt.json b/homeassistant/components/nest/translations/pt.json index 6da647ac29b..33ff857af7e 100644 --- a/homeassistant/components/nest/translations/pt.json +++ b/homeassistant/components/nest/translations/pt.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_fail": "Erro desconhecido ao gerar um URL de autoriza\u00e7\u00e3o.", - "authorize_url_timeout": "Limite temporal ultrapassado ao gerar um URL de autoriza\u00e7\u00e3o.", + "authorize_url_timeout": "Tempo excedido a gerar um URL de autoriza\u00e7\u00e3o", "missing_configuration": "O componente n\u00e3o est\u00e1 configurado. Por favor, siga a documenta\u00e7\u00e3o.", "no_url_available": "Nenhum URL dispon\u00edvel. Para obter informa\u00e7\u00f5es sobre esse erro, [verifique a sec\u00e7\u00e3o de ajuda]({docs_url})", "single_instance_allowed": "J\u00e1 configurado. Apenas uma \u00fanica configura\u00e7\u00e3o \u00e9 poss\u00edvel.", @@ -36,5 +36,10 @@ "title": "Escolha o m\u00e9todo de autentica\u00e7\u00e3o" } } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "Movimento detectado" + } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/tr.json b/homeassistant/components/nest/translations/tr.json index 484cdaff6ec..003c1ccc0c2 100644 --- a/homeassistant/components/nest/translations/tr.json +++ b/homeassistant/components/nest/translations/tr.json @@ -1,9 +1,20 @@ { + "config": { + "abort": { + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "unknown_authorize_url_generation": "Yetkilendirme url'si olu\u015fturulurken bilinmeyen hata." + }, + "error": { + "unknown": "Beklenmeyen hata" + } + }, "device_automation": { "trigger_type": { "camera_motion": "Hareket alg\u0131land\u0131", "camera_person": "Ki\u015fi alg\u0131land\u0131", - "camera_sound": "Ses alg\u0131land\u0131" + "camera_sound": "Ses alg\u0131land\u0131", + "doorbell_chime": "Kap\u0131 zili bas\u0131ld\u0131" } } } \ No newline at end of file diff --git a/homeassistant/components/nest/translations/uk.json b/homeassistant/components/nest/translations/uk.json new file mode 100644 index 00000000000..f2869a76f42 --- /dev/null +++ b/homeassistant/components/nest/translations/uk.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "authorize_url_fail": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e", + "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.", + "unknown_authorize_url_generation": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457." + }, + "create_entry": { + "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." + }, + "error": { + "internal_error": "\u0412\u043d\u0443\u0442\u0440\u0456\u0448\u043d\u044f \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0438 \u043a\u043e\u0434\u0443.", + "invalid_pin": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 PIN-\u043a\u043e\u0434.", + "timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0438 \u043a\u043e\u0434\u0443.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "init": { + "data": { + "flow_impl": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457", + "title": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + }, + "link": { + "data": { + "code": "PIN-\u043a\u043e\u0434" + }, + "description": "[\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0439\u0442\u0435\u0441\u044c]({url}), \u0449\u043e\u0431 \u043f\u0440\u0438\u0432'\u044f\u0437\u0430\u0442\u0438 \u0441\u0432\u043e\u044e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 Nest. \n \n\u041f\u0456\u0441\u043b\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457 \u0441\u043a\u043e\u043f\u0456\u044e\u0439\u0442\u0435 \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u0438\u0439 PIN-\u043a\u043e\u0434.", + "title": "\u041f\u0440\u0438\u0432'\u044f\u0437\u0430\u0442\u0438 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 Nest" + }, + "pick_implementation": { + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + }, + "reauth_confirm": { + "description": "\u041d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 Nest", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0432\u0430\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e" + } + } + }, + "device_automation": { + "trigger_type": { + "camera_motion": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u0440\u0443\u0445", + "camera_person": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c \u043b\u044e\u0434\u0438\u043d\u0438", + "camera_sound": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u0437\u0432\u0443\u043a", + "doorbell_chime": "\u041d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430 \u0434\u0432\u0435\u0440\u043d\u043e\u0433\u043e \u0434\u0437\u0432\u0456\u043d\u043a\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 30ce38753c6..dee8d3b668d 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -31,6 +31,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( ATTR_HEATING_POWER_REQUEST, ATTR_SCHEDULE_NAME, + ATTR_SELECTED_SCHEDULE, DATA_HANDLER, DATA_HOMES, DATA_SCHEDULES, @@ -212,6 +213,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self._hg_temperature = None self._boilerstatus = None self._setpoint_duration = None + self._selected_schedule = None if self._model == NA_THERM: self._operation_list.append(HVAC_MODE_OFF) @@ -243,7 +245,11 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): return home = data["home"] - if self._home_id == home["id"] and data["event_type"] == EVENT_TYPE_THERM_MODE: + + if self._home_id != home["id"]: + return + + if data["event_type"] == EVENT_TYPE_THERM_MODE: self._preset = NETATMO_MAP_PRESET[home[EVENT_TYPE_THERM_MODE]] self._hvac_mode = HVAC_MAP_NETATMO[self._preset] if self._preset == PRESET_FROST_GUARD: @@ -266,8 +272,13 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): elif room["therm_setpoint_mode"] == STATE_NETATMO_MAX: self._hvac_mode = HVAC_MODE_HEAT self._target_temperature = DEFAULT_MAX_TEMP + elif room["therm_setpoint_mode"] == STATE_NETATMO_MANUAL: + self._hvac_mode = HVAC_MODE_HEAT + self._target_temperature = room["therm_setpoint_temperature"] else: self._target_temperature = room["therm_setpoint_temperature"] + if self._target_temperature == DEFAULT_MAX_TEMP: + self._hvac_mode = HVAC_MODE_HEAT self.async_write_ha_state() break @@ -341,12 +352,28 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): STATE_NETATMO_HOME, ) - if preset_mode in [PRESET_BOOST, STATE_NETATMO_MAX] and self._model == NA_VALVE: + if ( + preset_mode in [PRESET_BOOST, STATE_NETATMO_MAX] + and self._model == NA_VALVE + and self.hvac_mode == HVAC_MODE_HEAT + ): + self._home_status.set_room_thermpoint( + self._id, + STATE_NETATMO_HOME, + ) + elif ( + preset_mode in [PRESET_BOOST, STATE_NETATMO_MAX] and self._model == NA_VALVE + ): self._home_status.set_room_thermpoint( self._id, STATE_NETATMO_MANUAL, DEFAULT_MAX_TEMP, ) + elif ( + preset_mode in [PRESET_BOOST, STATE_NETATMO_MAX] + and self.hvac_mode == HVAC_MODE_HEAT + ): + self._home_status.set_room_thermpoint(self._id, STATE_NETATMO_HOME) elif preset_mode in [PRESET_BOOST, STATE_NETATMO_MAX]: self._home_status.set_room_thermpoint( self._id, PRESET_MAP_NETATMO[preset_mode] @@ -390,6 +417,9 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): "heating_power_request", 0 ) + if self._selected_schedule is not None: + attr[ATTR_SELECTED_SCHEDULE] = self._selected_schedule + return attr def turn_off(self): @@ -438,6 +468,7 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): self._away_temperature = self._data.get_away_temp(self._home_id) self._hg_temperature = self._data.get_hg_temp(self._home_id) self._setpoint_duration = self._data.setpoint_duration[self._home_id] + self._selected_schedule = roomstatus.get("selected_schedule") if "current_temperature" not in roomstatus: return @@ -467,6 +498,11 @@ class NetatmoThermostat(NetatmoBase, ClimateEntity): "module_id": None, "heating_status": None, "heating_power_request": None, + "selected_schedule": self._data._get_selected_schedule( # pylint: disable=protected-access + home_id=self._home_id + ).get( + "name" + ), } batterylevel = None diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 138065f086b..ed1c5f0a880 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -69,6 +69,7 @@ ATTR_IS_KNOWN = "is_known" ATTR_FACE_URL = "face_url" ATTR_SCHEDULE_ID = "schedule_id" ATTR_SCHEDULE_NAME = "schedule_name" +ATTR_SELECTED_SCHEDULE = "selected_schedule" ATTR_CAMERA_LIGHT_MODE = "camera_light_mode" SERVICE_SET_CAMERA_LIGHT = "set_camera_light" diff --git a/homeassistant/components/netatmo/netatmo_entity_base.py b/homeassistant/components/netatmo/netatmo_entity_base.py index db2f8877f61..d0753613555 100644 --- a/homeassistant/components/netatmo/netatmo_entity_base.py +++ b/homeassistant/components/netatmo/netatmo_entity_base.py @@ -72,16 +72,6 @@ class NetatmoBase(Entity): data_class[SIGNAL_NAME], self.async_update_callback ) - async def async_remove(self): - """Clean up when removing entity.""" - entity_registry = await self.hass.helpers.entity_registry.async_get_registry() - entity_entry = entity_registry.async_get(self.entity_id) - if not entity_entry: - await super().async_remove() - return - - entity_registry.async_remove(self.entity_id) - @callback def async_update_callback(self): """Update the entity's state.""" diff --git a/homeassistant/components/netatmo/translations/de.json b/homeassistant/components/netatmo/translations/de.json index 30cfba6dfed..0be425d1e31 100644 --- a/homeassistant/components/netatmo/translations/de.json +++ b/homeassistant/components/netatmo/translations/de.json @@ -1,8 +1,10 @@ { "config": { "abort": { - "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Autorisierungs-URL.", - "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte folgen Sie der Dokumentation." + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "create_entry": { "default": "Erfolgreich authentifiziert." diff --git a/homeassistant/components/netatmo/translations/tr.json b/homeassistant/components/netatmo/translations/tr.json new file mode 100644 index 00000000000..94dd5b3fb0f --- /dev/null +++ b/homeassistant/components/netatmo/translations/tr.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + } + }, + "options": { + "step": { + "public_weather": { + "data": { + "area_name": "Alan\u0131n ad\u0131", + "lat_ne": "Enlem Kuzey-Do\u011fu k\u00f6\u015fesi", + "lat_sw": "Enlem G\u00fcney-Bat\u0131 k\u00f6\u015fesi", + "lon_ne": "Boylam Kuzey-Do\u011fu k\u00f6\u015fesi", + "lon_sw": "Boylam G\u00fcney-Bat\u0131 k\u00f6\u015fesi", + "mode": "Hesaplama", + "show_on_map": "Haritada g\u00f6ster" + }, + "description": "Bir alan i\u00e7in genel hava durumu sens\u00f6r\u00fc yap\u0131land\u0131r\u0131n.", + "title": "Netatmo genel hava durumu sens\u00f6r\u00fc" + }, + "public_weather_areas": { + "data": { + "new_area": "Alan ad\u0131", + "weather_areas": "Hava alanlar\u0131" + }, + "description": "Genel hava durumu sens\u00f6rlerini yap\u0131land\u0131r\u0131n.", + "title": "Netatmo genel hava durumu sens\u00f6r\u00fc" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/netatmo/translations/uk.json b/homeassistant/components/netatmo/translations/uk.json new file mode 100644 index 00000000000..b8c439edfde --- /dev/null +++ b/homeassistant/components/netatmo/translations/uk.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.", + "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." + }, + "create_entry": { + "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." + }, + "step": { + "pick_implementation": { + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + } + } + }, + "options": { + "step": { + "public_weather": { + "data": { + "area_name": "\u041d\u0430\u0437\u0432\u0430 \u043e\u0431\u043b\u0430\u0441\u0442\u0456", + "lat_ne": "\u0428\u0438\u0440\u043e\u0442\u0430 (\u043f\u0456\u0432\u043d\u0456\u0447\u043d\u043e-\u0441\u0445\u0456\u0434\u043d\u0438\u0439 \u043a\u0443\u0442)", + "lat_sw": "\u0428\u0438\u0440\u043e\u0442\u0430 (\u044e\u0433\u043e-\u0437\u0430\u043f\u0430\u0434\u043d\u044b\u0439 \u0443\u0433\u043e\u043b)", + "lon_ne": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430 (\u043f\u0456\u0432\u043d\u0456\u0447\u043d\u043e-\u0441\u0445\u0456\u0434\u043d\u0438\u0439 \u043a\u0443\u0442)", + "lon_sw": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430 (\u043f\u0456\u0432\u0434\u0435\u043d\u043d\u043e-\u0437\u0430\u0445\u0456\u0434\u043d\u0438\u0439 \u043a\u0443\u0442)", + "mode": "\u0420\u043e\u0437\u0440\u0430\u0445\u0443\u043d\u043e\u043a", + "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438 \u043d\u0430 \u043c\u0430\u043f\u0456" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u0437\u0430\u0433\u0430\u043b\u044c\u043d\u043e\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0434\u0430\u0442\u0447\u0438\u043a \u043f\u043e\u0433\u043e\u0434\u0438 \u0434\u043b\u044f \u043e\u0431\u043b\u0430\u0441\u0442\u0456", + "title": "\u0417\u0430\u0433\u0430\u043b\u044c\u043d\u043e\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0434\u0430\u0442\u0447\u0438\u043a \u043f\u043e\u0433\u043e\u0434\u0438 Netatmo" + }, + "public_weather_areas": { + "data": { + "new_area": "\u041d\u0430\u0437\u0432\u0430 \u043e\u0431\u043b\u0430\u0441\u0442\u0456", + "weather_areas": "\u041f\u043e\u0433\u043e\u0434\u043d\u0456 \u043e\u0431\u043b\u0430\u0441\u0442\u0456" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u0430\u0433\u0430\u043b\u044c\u043d\u043e\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0445 \u0434\u0430\u0442\u0447\u0438\u043a\u0456\u0432 \u043f\u043e\u0433\u043e\u0434\u0438", + "title": "\u0417\u0430\u0433\u0430\u043b\u044c\u043d\u043e\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0434\u0430\u0442\u0447\u0438\u043a \u043f\u043e\u0433\u043e\u0434\u0438 Netatmo" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/__init__.py b/homeassistant/components/nexia/__init__.py index 54e6ddd92b3..fc2ef7fef35 100644 --- a/homeassistant/components/nexia/__init__.py +++ b/homeassistant/components/nexia/__init__.py @@ -6,9 +6,8 @@ import logging from nexia.home import NexiaHome from requests.exceptions import ConnectTimeout, HTTPError -import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -20,19 +19,7 @@ from .util import is_invalid_auth_code _LOGGER = logging.getLogger(__name__) - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - }, - extra=vol.ALLOW_EXTRA, - ) - }, - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = cv.deprecated(DOMAIN) DEFAULT_UPDATE_RATE = 120 @@ -40,17 +27,8 @@ DEFAULT_UPDATE_RATE = 120 async def async_setup(hass: HomeAssistant, config: dict) -> bool: """Set up the nexia component from YAML.""" - conf = config.get(DOMAIN) hass.data.setdefault(DOMAIN, {}) - if not conf: - return True - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=conf - ) - ) return True diff --git a/homeassistant/components/nexia/config_flow.py b/homeassistant/components/nexia/config_flow.py index 2054a5de421..87850145077 100644 --- a/homeassistant/components/nexia/config_flow.py +++ b/homeassistant/components/nexia/config_flow.py @@ -79,13 +79,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_import(self, user_input): - """Handle import.""" - for entry in self._async_current_entries(): - if entry.data[CONF_USERNAME] == user_input[CONF_USERNAME]: - return self.async_abort(reason="already_configured") - return await self.async_step_user(user_input) - class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index 1a4d6c74e84..cb3493ebc55 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,8 +1,9 @@ { "domain": "nexia", "name": "Nexia", - "requirements": ["nexia==0.9.4"], - "codeowners": ["@ryannazaretian", "@bdraco"], + "requirements": ["nexia==0.9.5"], + "codeowners": ["@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", - "config_flow": true + "config_flow": true, + "dhcp": [{"hostname":"xl857-*","macaddress":"000231*"}] } diff --git a/homeassistant/components/nexia/translations/de.json b/homeassistant/components/nexia/translations/de.json index 0ff4da3b2e1..f2220f828e8 100644 --- a/homeassistant/components/nexia/translations/de.json +++ b/homeassistant/components/nexia/translations/de.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Dieses Nexia Home ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/nexia/translations/tr.json b/homeassistant/components/nexia/translations/tr.json new file mode 100644 index 00000000000..47f3d931c46 --- /dev/null +++ b/homeassistant/components/nexia/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "title": "Mynexia.com'a ba\u011flan\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/translations/uk.json b/homeassistant/components/nexia/translations/uk.json new file mode 100644 index 00000000000..8cb2aec836a --- /dev/null +++ b/homeassistant/components/nexia/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e mynexia.com" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nightscout/translations/de.json b/homeassistant/components/nightscout/translations/de.json index 8581b04099d..510d57ce45f 100644 --- a/homeassistant/components/nightscout/translations/de.json +++ b/homeassistant/components/nightscout/translations/de.json @@ -1,12 +1,18 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "flow_title": "Nightscout", "step": { "user": { "data": { + "api_key": "API-Schl\u00fcssel", "url": "URL" } } diff --git a/homeassistant/components/nightscout/translations/no.json b/homeassistant/components/nightscout/translations/no.json index db7b8f811ca..d68fe45c684 100644 --- a/homeassistant/components/nightscout/translations/no.json +++ b/homeassistant/components/nightscout/translations/no.json @@ -15,7 +15,7 @@ "api_key": "API-n\u00f8kkel", "url": "URL" }, - "description": "- URL: adressen til din nattscout-forekomst. Dvs: https://myhomeassistant.duckdns.org:5423 \n - API-n\u00f8kkel (valgfritt): Bruk bare hvis forekomsten din er beskyttet (auth_default_roles! = Lesbar).", + "description": "- URL: Adressen til din nattscout-forekomst. F. Eks: https://myhomeassistant.duckdns.org:5423 \n- API-n\u00f8kkel (valgfritt): Bruk bare hvis forekomsten din er beskyttet (auth_default_roles! = readable).", "title": "Skriv inn informasjon om Nightscout-serveren." } } diff --git a/homeassistant/components/nightscout/translations/tr.json b/homeassistant/components/nightscout/translations/tr.json index 585aace899d..95f36a4d124 100644 --- a/homeassistant/components/nightscout/translations/tr.json +++ b/homeassistant/components/nightscout/translations/tr.json @@ -1,11 +1,18 @@ { "config": { - "error": { - "cannot_connect": "Ba\u011flan\u0131lamad\u0131" + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" }, + "error": { + "cannot_connect": "Ba\u011flan\u0131lamad\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "flow_title": "Nightscout", "step": { "user": { "data": { + "api_key": "API Anahtar\u0131", "url": "URL" } } diff --git a/homeassistant/components/nightscout/translations/uk.json b/homeassistant/components/nightscout/translations/uk.json new file mode 100644 index 00000000000..6504b00eb88 --- /dev/null +++ b/homeassistant/components/nightscout/translations/uk.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "flow_title": "Nightscout", + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "url": "URL-\u0430\u0434\u0440\u0435\u0441\u0430" + }, + "description": "- URL: \u0430\u0434\u0440\u0435\u0441\u0430 \u0412\u0430\u0448\u043e\u0433\u043e Nightscout. \u041d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: https://myhomeassistant.duckdns.org:5423\n - \u041a\u043b\u044e\u0447 API (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e): \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435, \u043b\u0438\u0448\u0435 \u044f\u043a\u0449\u043e \u0412\u0430\u0448 Nightcout \u0437\u0430\u0445\u0438\u0449\u0435\u043d\u0438\u0439 (auth_default_roles != readable).", + "title": "Nightscout" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 426c28bccff..d3439baf4fb 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -173,7 +173,10 @@ class BaseNotificationService: target_name = slugify(f"{self._target_service_name_prefix}_{name}") if target_name in stale_targets: stale_targets.remove(target_name) - if target_name in self.registered_targets: + if ( + target_name in self.registered_targets + and target == self.registered_targets[target_name] + ): continue self.registered_targets[target_name] = target self.hass.services.async_register( diff --git a/homeassistant/components/notify/translations/uk.json b/homeassistant/components/notify/translations/uk.json index 86821a3e50f..d87752255d5 100644 --- a/homeassistant/components/notify/translations/uk.json +++ b/homeassistant/components/notify/translations/uk.json @@ -1,3 +1,3 @@ { - "title": "\u041f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u043d\u044f" + "title": "\u0421\u043f\u043e\u0432\u0456\u0449\u0435\u043d\u043d\u044f" } \ No newline at end of file diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 88da19f5ab2..c2cbdb85289 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -171,6 +171,7 @@ class NotionEntity(CoordinatorEntity): return ( self.coordinator.last_update_success and self.task_id in self.coordinator.data["tasks"] + and self._state ) @property @@ -230,10 +231,10 @@ class NotionEntity(CoordinatorEntity): device_registry = await dr.async_get_registry(self.hass) bridge = self.coordinator.data["bridges"][self._bridge_id] bridge_device = device_registry.async_get_device( - {DOMAIN: bridge["hardware_id"]}, set() + {(DOMAIN, bridge["hardware_id"])} ) this_device = device_registry.async_get_device( - {DOMAIN: sensor["hardware_id"]}, set() + {(DOMAIN, sensor["hardware_id"])} ) device_registry.async_update_device( diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py index a198903b99a..74ad724c50b 100644 --- a/homeassistant/components/notion/binary_sensor.py +++ b/homeassistant/components/notion/binary_sensor.py @@ -16,6 +16,7 @@ from . import NotionEntity from .const import ( DATA_COORDINATOR, DOMAIN, + LOGGER, SENSOR_BATTERY, SENSOR_DOOR, SENSOR_GARAGE_DOOR, @@ -81,8 +82,11 @@ class NotionBinarySensor(NotionEntity, BinarySensorEntity): if "value" in task["status"]: self._state = task["status"]["value"] - elif task["task_type"] == SENSOR_BATTERY: - self._state = task["status"]["data"]["to_state"] + elif task["status"].get("insights", {}).get("primary"): + self._state = task["status"]["insights"]["primary"]["to_state"] + else: + LOGGER.warning("Unknown data payload: %s", task["status"]) + self._state = None @property def is_on(self) -> bool: diff --git a/homeassistant/components/notion/translations/de.json b/homeassistant/components/notion/translations/de.json index f322826c45b..0b421911aa7 100644 --- a/homeassistant/components/notion/translations/de.json +++ b/homeassistant/components/notion/translations/de.json @@ -1,9 +1,10 @@ { "config": { "abort": { - "already_configured": "Dieser Benutzername wird bereits benutzt." + "already_configured": "Konto wurde bereits konfiguriert" }, "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung", "no_devices": "Keine Ger\u00e4te im Konto gefunden" }, "step": { diff --git a/homeassistant/components/notion/translations/tr.json b/homeassistant/components/notion/translations/tr.json index 8966b79df1b..f89e3fb7533 100644 --- a/homeassistant/components/notion/translations/tr.json +++ b/homeassistant/components/notion/translations/tr.json @@ -1,7 +1,19 @@ { "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", "no_devices": "Hesapta cihaz bulunamad\u0131" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/notion/translations/uk.json b/homeassistant/components/notion/translations/uk.json new file mode 100644 index 00000000000..6dc969c3609 --- /dev/null +++ b/homeassistant/components/notion/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "no_devices": "\u041d\u0435\u043c\u0430\u0454 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432, \u043f\u043e\u0432'\u044f\u0437\u0430\u043d\u0438\u0445 \u0437 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u043c \u0437\u0430\u043f\u0438\u0441\u043e\u043c." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "Notion" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/__init__.py b/homeassistant/components/nuheat/__init__.py index 0bb8ed2ddaa..2f51ae377a5 100644 --- a/homeassistant/components/nuheat/__init__.py +++ b/homeassistant/components/nuheat/__init__.py @@ -5,11 +5,9 @@ import logging import nuheat import requests -import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( - CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME, HTTP_BAD_REQUEST, @@ -24,49 +22,12 @@ from .const import CONF_SERIAL_NUMBER, DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_DEVICES, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), - } - ) - }, - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = cv.deprecated(DOMAIN) async def async_setup(hass: HomeAssistant, config: dict): """Set up the NuHeat component.""" hass.data.setdefault(DOMAIN, {}) - conf = config.get(DOMAIN) - if not conf: - return True - - for serial_number in conf[CONF_DEVICES]: - # Since the api currently doesn't permit fetching the serial numbers - # and they have to be specified we create a separate config entry for - # each serial number. This won't increase the number of http - # requests as each thermostat has to be updated anyways. - # This also allows us to validate that the entered valid serial - # numbers and do not end up with a config entry where half of the - # devices work. - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_USERNAME: conf[CONF_USERNAME], - CONF_PASSWORD: conf[CONF_PASSWORD], - CONF_SERIAL_NUMBER: serial_number, - }, - ) - ) - return True diff --git a/homeassistant/components/nuheat/config_flow.py b/homeassistant/components/nuheat/config_flow.py index eb76c620767..40de3844620 100644 --- a/homeassistant/components/nuheat/config_flow.py +++ b/homeassistant/components/nuheat/config_flow.py @@ -92,13 +92,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - async def async_step_import(self, user_input): - """Handle import.""" - await self.async_set_unique_id(user_input[CONF_SERIAL_NUMBER]) - self._abort_if_unique_id_configured() - - return await self.async_step_user(user_input) - class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/nuheat/manifest.json b/homeassistant/components/nuheat/manifest.json index d479f570d60..92527f50660 100644 --- a/homeassistant/components/nuheat/manifest.json +++ b/homeassistant/components/nuheat/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/nuheat", "requirements": ["nuheat==0.3.0"], "codeowners": ["@bdraco"], - "config_flow": true + "config_flow": true, + "dhcp": [{"hostname":"nuheat","macaddress":"002338*"}] } diff --git a/homeassistant/components/nuheat/translations/de.json b/homeassistant/components/nuheat/translations/de.json index 52c30681efc..8599f7fe1b5 100644 --- a/homeassistant/components/nuheat/translations/de.json +++ b/homeassistant/components/nuheat/translations/de.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Der Thermostat ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "invalid_thermostat": "Die Seriennummer des Thermostats ist ung\u00fcltig.", "unknown": "Unerwarteter Fehler" diff --git a/homeassistant/components/nuheat/translations/tr.json b/homeassistant/components/nuheat/translations/tr.json new file mode 100644 index 00000000000..5123f1c7d9a --- /dev/null +++ b/homeassistant/components/nuheat/translations/tr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "invalid_thermostat": "Termostat seri numaras\u0131 ge\u00e7ersiz.", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "serial_number": "Termostat\u0131n seri numaras\u0131.", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/translations/uk.json b/homeassistant/components/nuheat/translations/uk.json new file mode 100644 index 00000000000..21be3968eb7 --- /dev/null +++ b/homeassistant/components/nuheat/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "invalid_thermostat": "\u0421\u0435\u0440\u0456\u0439\u043d\u0438\u0439 \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u0440\u043c\u043e\u0441\u0442\u0430\u0442\u0430 \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "serial_number": "\u0421\u0435\u0440\u0456\u0439\u043d\u0438\u0439 \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u0440\u043c\u043e\u0441\u0442\u0430\u0442\u0430", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0441\u0435\u0440\u0456\u0439\u043d\u0438\u0439 \u043d\u043e\u043c\u0435\u0440 \u0430\u0431\u043e ID \u0412\u0430\u0448\u043e\u0433\u043e \u0442\u0435\u0440\u043c\u043e\u0441\u0442\u0430\u0442\u0430, \u043d\u0430 \u0441\u0430\u0439\u0442\u0456 https://MyNuHeat.com.", + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/ca.json b/homeassistant/components/nuki/translations/ca.json new file mode 100644 index 00000000000..a08308e7897 --- /dev/null +++ b/homeassistant/components/nuki/translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port", + "token": "Token d'acc\u00e9s" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/cs.json b/homeassistant/components/nuki/translations/cs.json new file mode 100644 index 00000000000..349c92805cf --- /dev/null +++ b/homeassistant/components/nuki/translations/cs.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "user": { + "data": { + "host": "Hostitel", + "port": "Port", + "token": "P\u0159\u00edstupov\u00fd token" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/en.json b/homeassistant/components/nuki/translations/en.json new file mode 100644 index 00000000000..135e8de2b2f --- /dev/null +++ b/homeassistant/components/nuki/translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port", + "token": "Access Token" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/et.json b/homeassistant/components/nuki/translations/et.json new file mode 100644 index 00000000000..750afff003c --- /dev/null +++ b/homeassistant/components/nuki/translations/et.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Vigane autentimine", + "unknown": "Ootamatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port", + "token": "Juurdep\u00e4\u00e4sut\u00f5end" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/it.json b/homeassistant/components/nuki/translations/it.json new file mode 100644 index 00000000000..899093e1f41 --- /dev/null +++ b/homeassistant/components/nuki/translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Porta", + "token": "Token di accesso" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/no.json b/homeassistant/components/nuki/translations/no.json new file mode 100644 index 00000000000..8cdbac230d7 --- /dev/null +++ b/homeassistant/components/nuki/translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert", + "port": "Port", + "token": "Tilgangstoken" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/pl.json b/homeassistant/components/nuki/translations/pl.json new file mode 100644 index 00000000000..77a7c31ee34 --- /dev/null +++ b/homeassistant/components/nuki/translations/pl.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "port": "Port", + "token": "Token dost\u0119pu" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/ru.json b/homeassistant/components/nuki/translations/ru.json new file mode 100644 index 00000000000..bad9f35c076 --- /dev/null +++ b/homeassistant/components/nuki/translations/ru.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442", + "token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/tr.json b/homeassistant/components/nuki/translations/tr.json new file mode 100644 index 00000000000..ba6a496fa4c --- /dev/null +++ b/homeassistant/components/nuki/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port", + "token": "Eri\u015fim Belirteci" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuki/translations/zh-Hant.json b/homeassistant/components/nuki/translations/zh-Hant.json new file mode 100644 index 00000000000..662d7ed6ed9 --- /dev/null +++ b/homeassistant/components/nuki/translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0", + "token": "\u5b58\u53d6\u5bc6\u9470" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/number/device_action.py b/homeassistant/components/number/device_action.py new file mode 100644 index 00000000000..c22ba720e37 --- /dev/null +++ b/homeassistant/components/number/device_action.py @@ -0,0 +1,82 @@ +"""Provides device actions for Number.""" +from typing import Any, Dict, List, Optional + +import voluptuous as vol + +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_TYPE, +) +from homeassistant.core import Context, HomeAssistant +from homeassistant.helpers import entity_registry +import homeassistant.helpers.config_validation as cv + +from . import DOMAIN, const + +ATYP_SET_VALUE = "set_value" + +ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): ATYP_SET_VALUE, + vol.Required(CONF_ENTITY_ID): cv.entity_domain(DOMAIN), + vol.Required(const.ATTR_VALUE): vol.Coerce(float), + } +) + + +async def async_get_actions(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device actions for Number.""" + registry = await entity_registry.async_get_registry(hass) + actions: List[Dict[str, Any]] = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + actions.append( + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: ATYP_SET_VALUE, + } + ) + + return actions + + +async def async_call_action_from_config( + hass: HomeAssistant, config: dict, variables: dict, context: Optional[Context] +) -> None: + """Execute a device action.""" + config = ACTION_SCHEMA(config) + + if config[CONF_TYPE] != ATYP_SET_VALUE: + return + + await hass.services.async_call( + DOMAIN, + const.SERVICE_SET_VALUE, + { + ATTR_ENTITY_ID: config[CONF_ENTITY_ID], + const.ATTR_VALUE: config[const.ATTR_VALUE], + }, + blocking=True, + context=context, + ) + + +async def async_get_action_capabilities(hass: HomeAssistant, config: dict) -> dict: + """List action capabilities.""" + action_type = config[CONF_TYPE] + + if action_type != ATYP_SET_VALUE: + return {} + + fields = {vol.Required(const.ATTR_VALUE): vol.Coerce(float)} + + return {"extra_fields": vol.Schema(fields)} diff --git a/homeassistant/components/number/strings.json b/homeassistant/components/number/strings.json new file mode 100644 index 00000000000..77ba7e7a913 --- /dev/null +++ b/homeassistant/components/number/strings.json @@ -0,0 +1,8 @@ +{ + "title": "Number", + "device_automation": { + "action_type": { + "set_value": "Set value for {entity_name}" + } + } +} diff --git a/homeassistant/components/number/translations/ca.json b/homeassistant/components/number/translations/ca.json new file mode 100644 index 00000000000..0058f01aac0 --- /dev/null +++ b/homeassistant/components/number/translations/ca.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "Estableix el valor de {entity_name}" + } + }, + "title": "N\u00famero" +} \ No newline at end of file diff --git a/homeassistant/components/number/translations/cs.json b/homeassistant/components/number/translations/cs.json new file mode 100644 index 00000000000..a6810f08c61 --- /dev/null +++ b/homeassistant/components/number/translations/cs.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "Nastavit hodnotu pro {entity_name}" + } + }, + "title": "\u010c\u00edslo" +} \ No newline at end of file diff --git a/homeassistant/components/number/translations/en.json b/homeassistant/components/number/translations/en.json new file mode 100644 index 00000000000..4e3fe6536b3 --- /dev/null +++ b/homeassistant/components/number/translations/en.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "Set value for {entity_name}" + } + }, + "title": "Number" +} \ No newline at end of file diff --git a/homeassistant/components/number/translations/et.json b/homeassistant/components/number/translations/et.json new file mode 100644 index 00000000000..36958c0fc77 --- /dev/null +++ b/homeassistant/components/number/translations/et.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "Olemi {entity_name} v\u00e4\u00e4rtuse m\u00e4\u00e4ramine" + } + }, + "title": "Number" +} \ No newline at end of file diff --git a/homeassistant/components/number/translations/it.json b/homeassistant/components/number/translations/it.json new file mode 100644 index 00000000000..135467cea9b --- /dev/null +++ b/homeassistant/components/number/translations/it.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "Imposta il valore per {entity_name}" + } + }, + "title": "Numero" +} \ No newline at end of file diff --git a/homeassistant/components/number/translations/no.json b/homeassistant/components/number/translations/no.json new file mode 100644 index 00000000000..ad82c4ac6d1 --- /dev/null +++ b/homeassistant/components/number/translations/no.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "Angi verdi for {entity_name}" + } + }, + "title": "Nummer" +} \ No newline at end of file diff --git a/homeassistant/components/number/translations/pl.json b/homeassistant/components/number/translations/pl.json new file mode 100644 index 00000000000..93d5dd04599 --- /dev/null +++ b/homeassistant/components/number/translations/pl.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "ustaw warto\u015b\u0107 dla {entity_name}" + } + }, + "title": "Number" +} \ No newline at end of file diff --git a/homeassistant/components/number/translations/ru.json b/homeassistant/components/number/translations/ru.json new file mode 100644 index 00000000000..5e250b4e2db --- /dev/null +++ b/homeassistant/components/number/translations/ru.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u0435 \u0434\u043b\u044f {entity_name}" + } + }, + "title": "\u0427\u0438\u0441\u043b\u043e" +} \ No newline at end of file diff --git a/homeassistant/components/number/translations/tr.json b/homeassistant/components/number/translations/tr.json new file mode 100644 index 00000000000..dfdbd905317 --- /dev/null +++ b/homeassistant/components/number/translations/tr.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "{entity_name} i\u00e7in de\u011fer ayarlay\u0131n" + } + }, + "title": "Numara" +} \ No newline at end of file diff --git a/homeassistant/components/number/translations/zh-Hant.json b/homeassistant/components/number/translations/zh-Hant.json new file mode 100644 index 00000000000..d36f751682d --- /dev/null +++ b/homeassistant/components/number/translations/zh-Hant.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "set_value": "{entity_name} \u8a2d\u5b9a\u503c" + } + }, + "title": "\u865f\u78bc" +} \ No newline at end of file diff --git a/homeassistant/components/nut/config_flow.py b/homeassistant/components/nut/config_flow.py index 33306e24acb..8a868d7bb39 100644 --- a/homeassistant/components/nut/config_flow.py +++ b/homeassistant/components/nut/config_flow.py @@ -136,22 +136,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): } return await self.async_step_user() - async def async_step_import(self, user_input=None): - """Handle the import.""" - errors = {} - if user_input is not None: - if self._host_port_alias_already_configured(user_input): - return self.async_abort(reason="already_configured") - _, errors = await self._async_validate_or_error(user_input) - - if not errors: - title = _format_host_port_alias(user_input) - return self.async_create_entry(title=title, data=user_input) - - return self.async_show_form( - step_id="user", data_schema=_base_schema({}), errors=errors - ) - async def async_step_user(self, user_input=None): """Handle the user input.""" errors = {} diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index be98318edfd..f4fbbdef932 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -1,29 +1,11 @@ """Provides a sensor to track various status aspects of a UPS.""" import logging -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import ( - ATTR_STATE, - CONF_ALIAS, - CONF_HOST, - CONF_NAME, - CONF_PASSWORD, - CONF_PORT, - CONF_RESOURCES, - CONF_USERNAME, - STATE_UNKNOWN, -) -import homeassistant.helpers.config_validation as cv +from homeassistant.const import ATTR_STATE, CONF_RESOURCES, STATE_UNKNOWN from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( COORDINATOR, - DEFAULT_HOST, - DEFAULT_NAME, - DEFAULT_PORT, DOMAIN, KEY_STATUS, KEY_STATUS_DISPLAY, @@ -44,29 +26,6 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_ALIAS): cv.string, - vol.Optional(CONF_USERNAME): cv.string, - vol.Optional(CONF_PASSWORD): cv.string, - vol.Required(CONF_RESOURCES): vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), - } -) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Import the platform into a config entry.""" - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config - ) - ) - - async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the NUT sensors.""" diff --git a/homeassistant/components/nut/translations/de.json b/homeassistant/components/nut/translations/de.json index 793ab5bfa7c..50d37fa8ec4 100644 --- a/homeassistant/components/nut/translations/de.json +++ b/homeassistant/components/nut/translations/de.json @@ -4,7 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/nut/translations/tr.json b/homeassistant/components/nut/translations/tr.json new file mode 100644 index 00000000000..b383d765619 --- /dev/null +++ b/homeassistant/components/nut/translations/tr.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "resources": { + "data": { + "resources": "Kaynaklar" + } + }, + "ups": { + "data": { + "alias": "Takma ad" + } + }, + "user": { + "data": { + "host": "Ana Bilgisayar", + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "resources": "Kaynaklar" + }, + "description": "Sens\u00f6r Kaynaklar\u0131'n\u0131 se\u00e7in." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/translations/uk.json b/homeassistant/components/nut/translations/uk.json new file mode 100644 index 00000000000..b25fe854560 --- /dev/null +++ b/homeassistant/components/nut/translations/uk.json @@ -0,0 +1,46 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "resources": { + "data": { + "resources": "\u0420\u0435\u0441\u0443\u0440\u0441\u0438" + }, + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0440\u0435\u0441\u0443\u0440\u0441\u0438 \u0434\u043b\u044f \u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433\u0443" + }, + "ups": { + "data": { + "alias": "\u041f\u0441\u0435\u0432\u0434\u043e\u043d\u0456\u043c", + "resources": "\u0420\u0435\u0441\u0443\u0440\u0441\u0438" + }, + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c UPS \u0434\u043b\u044f \u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433\u0443" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 NUT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "resources": "\u0420\u0435\u0441\u0443\u0440\u0441\u0438", + "scan_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u0441\u043a\u0430\u043d\u0443\u0432\u0430\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0440\u0435\u0441\u0443\u0440\u0441\u0438 \u0441\u0435\u043d\u0441\u043e\u0440\u0456\u0432." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nws/translations/de.json b/homeassistant/components/nws/translations/de.json index 1461d86b2e5..3d409bf885b 100644 --- a/homeassistant/components/nws/translations/de.json +++ b/homeassistant/components/nws/translations/de.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Ger\u00e4t ist bereits konfiguriert" + "already_configured": "Der Dienst ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/nws/translations/tr.json b/homeassistant/components/nws/translations/tr.json new file mode 100644 index 00000000000..8f51593aedb --- /dev/null +++ b/homeassistant/components/nws/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "latitude": "Enlem", + "longitude": "Boylam" + }, + "description": "Bir METAR istasyon kodu belirtilmezse, en yak\u0131n istasyonu bulmak i\u00e7in enlem ve boylam kullan\u0131lacakt\u0131r. \u015eimdilik bir API Anahtar\u0131 herhangi bir \u015fey olabilir. Ge\u00e7erli bir e-posta adresi kullanman\u0131z tavsiye edilir." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nws/translations/uk.json b/homeassistant/components/nws/translations/uk.json new file mode 100644 index 00000000000..1e6886540ae --- /dev/null +++ b/homeassistant/components/nws/translations/uk.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", + "station": "\u041a\u043e\u0434 \u0441\u0442\u0430\u043d\u0446\u0456\u0457 METAR" + }, + "description": "\u042f\u043a\u0449\u043e \u043a\u043e\u0434 \u0441\u0442\u0430\u043d\u0446\u0456\u0457 METAR \u043d\u0435 \u0432\u043a\u0430\u0437\u0430\u043d\u043e, \u0434\u043b\u044f \u043f\u043e\u0448\u0443\u043a\u0443 \u043d\u0430\u0439\u0431\u043b\u0438\u0436\u0447\u043e\u0457 \u0441\u0442\u0430\u043d\u0446\u0456\u0457 \u0431\u0443\u0434\u0443\u0442\u044c \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u0448\u0438\u0440\u043e\u0442\u0430 \u0456 \u0434\u043e\u0432\u0433\u043e\u0442\u0430. \u041d\u0430 \u0434\u0430\u043d\u0438\u0439 \u043c\u043e\u043c\u0435\u043d\u0442 \u043a\u043b\u044e\u0447 API \u043c\u043e\u0436\u0435 \u0431\u0443\u0442\u0438 \u0431\u0443\u0434\u044c-\u044f\u043a\u0438\u043c. \u0420\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0454\u0442\u044c\u0441\u044f \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0434\u0456\u044e\u0447\u0443 \u0430\u0434\u0440\u0435\u0441\u0443 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438.", + "title": "National Weather Service" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nzbget/translations/de.json b/homeassistant/components/nzbget/translations/de.json index 018f3870c58..529eff3d9a2 100644 --- a/homeassistant/components/nzbget/translations/de.json +++ b/homeassistant/components/nzbget/translations/de.json @@ -1,8 +1,12 @@ { "config": { "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", "unknown": "Unerwarteter Fehler" }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, "flow_title": "NZBGet: {name}", "step": { "user": { @@ -11,7 +15,9 @@ "name": "Name", "password": "Passwort", "port": "Port", - "username": "Benutzername" + "ssl": "Nutzt ein SSL-Zertifikat", + "username": "Benutzername", + "verify_ssl": "SSL-Zertifikat verfizieren" }, "title": "Mit NZBGet verbinden" } diff --git a/homeassistant/components/nzbget/translations/tr.json b/homeassistant/components/nzbget/translations/tr.json new file mode 100644 index 00000000000..63b6c489018 --- /dev/null +++ b/homeassistant/components/nzbget/translations/tr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "unknown": "Beklenmeyen hata" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "G\u00fcncelle\u015ftirme s\u0131kl\u0131\u011f\u0131 (saniye)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nzbget/translations/uk.json b/homeassistant/components/nzbget/translations/uk.json new file mode 100644 index 00000000000..eba15cca19c --- /dev/null +++ b/homeassistant/components/nzbget/translations/uk.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "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.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "flow_title": "NZBGet: {name}", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", + "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL" + }, + "title": "NZBGet" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/obihai/manifest.json b/homeassistant/components/obihai/manifest.json index bb72a967605..78123cc07f5 100644 --- a/homeassistant/components/obihai/manifest.json +++ b/homeassistant/components/obihai/manifest.json @@ -2,6 +2,6 @@ "domain": "obihai", "name": "Obihai", "documentation": "https://www.home-assistant.io/integrations/obihai", - "requirements": ["pyobihai==1.2.3"], + "requirements": ["pyobihai==1.3.1"], "codeowners": ["@dshokouhi"] } diff --git a/homeassistant/components/omnilogic/translations/de.json b/homeassistant/components/omnilogic/translations/de.json index 38215675701..4378d39912d 100644 --- a/homeassistant/components/omnilogic/translations/de.json +++ b/homeassistant/components/omnilogic/translations/de.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/omnilogic/translations/tr.json b/homeassistant/components/omnilogic/translations/tr.json new file mode 100644 index 00000000000..ab93b71de84 --- /dev/null +++ b/homeassistant/components/omnilogic/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/omnilogic/translations/uk.json b/homeassistant/components/omnilogic/translations/uk.json new file mode 100644 index 00000000000..21ebf6f4faf --- /dev/null +++ b/homeassistant/components/omnilogic/translations/uk.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "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." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "polling_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0438\u0442\u0443\u0432\u0430\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onboarding/translations/uk.json b/homeassistant/components/onboarding/translations/uk.json new file mode 100644 index 00000000000..595726cbd34 --- /dev/null +++ b/homeassistant/components/onboarding/translations/uk.json @@ -0,0 +1,7 @@ +{ + "area": { + "bedroom": "\u0421\u043f\u0430\u043b\u044c\u043d\u044f", + "kitchen": "\u041a\u0443\u0445\u043d\u044f", + "living_room": "\u0412\u0456\u0442\u0430\u043b\u044c\u043d\u044f" + } +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/__init__.py b/homeassistant/components/ondilo_ico/__init__.py new file mode 100644 index 00000000000..69538c5e8b3 --- /dev/null +++ b/homeassistant/components/ondilo_ico/__init__.py @@ -0,0 +1,59 @@ +"""The Ondilo ICO integration.""" +import asyncio + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_entry_oauth2_flow + +from . import api, config_flow +from .const import DOMAIN +from .oauth_impl import OndiloOauth2Implementation + +PLATFORMS = ["sensor"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Ondilo ICO component.""" + hass.data.setdefault(DOMAIN, {}) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Ondilo ICO from a config entry.""" + + config_flow.OAuth2FlowHandler.async_register_implementation( + hass, + OndiloOauth2Implementation(hass), + ) + + implementation = ( + await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + ) + + hass.data[DOMAIN][entry.entry_id] = api.OndiloClient(hass, entry, implementation) + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/ondilo_ico/api.py b/homeassistant/components/ondilo_ico/api.py new file mode 100644 index 00000000000..e753f8d6dcb --- /dev/null +++ b/homeassistant/components/ondilo_ico/api.py @@ -0,0 +1,50 @@ +"""API for Ondilo ICO bound to Home Assistant OAuth.""" +from asyncio import run_coroutine_threadsafe +import logging + +from ondilo import Ondilo + +from homeassistant import config_entries, core +from homeassistant.helpers import config_entry_oauth2_flow + +_LOGGER = logging.getLogger(__name__) + + +class OndiloClient(Ondilo): + """Provide Ondilo ICO authentication tied to an OAuth2 based config entry.""" + + def __init__( + self, + hass: core.HomeAssistant, + config_entry: config_entries.ConfigEntry, + implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation, + ): + """Initialize Ondilo ICO Auth.""" + self.hass = hass + self.config_entry = config_entry + self.session = config_entry_oauth2_flow.OAuth2Session( + hass, config_entry, implementation + ) + super().__init__(self.session.token) + + def refresh_tokens(self) -> dict: + """Refresh and return new Ondilo ICO tokens using Home Assistant OAuth2 session.""" + run_coroutine_threadsafe( + self.session.async_ensure_token_valid(), self.hass.loop + ).result() + + return self.session.token + + def get_all_pools_data(self) -> dict: + """Fetch pools and add pool details and last measures to pool data.""" + + pools = self.get_pools() + for pool in pools: + _LOGGER.debug( + "Retrieving data for pool/spa: %s, id: %d", pool["name"], pool["id"] + ) + pool["ICO"] = self.get_ICO_details(pool["id"]) + pool["sensors"] = self.get_last_pool_measures(pool["id"]) + _LOGGER.debug("Retrieved the following sensors data: %s", pool["sensors"]) + + return pools diff --git a/homeassistant/components/ondilo_ico/config_flow.py b/homeassistant/components/ondilo_ico/config_flow.py new file mode 100644 index 00000000000..c6a164e913b --- /dev/null +++ b/homeassistant/components/ondilo_ico/config_flow.py @@ -0,0 +1,43 @@ +"""Config flow for Ondilo ICO.""" +import logging + +from homeassistant import config_entries +from homeassistant.helpers import config_entry_oauth2_flow + +from .const import DOMAIN +from .oauth_impl import OndiloOauth2Implementation + +_LOGGER = logging.getLogger(__name__) + + +class OAuth2FlowHandler( + config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN +): + """Config flow to handle Ondilo ICO OAuth2 authentication.""" + + DOMAIN = DOMAIN + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle a flow initialized by the user.""" + await self.async_set_unique_id(DOMAIN) + + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + + self.async_register_implementation( + self.hass, + OndiloOauth2Implementation(self.hass), + ) + + return await super().async_step_user(user_input) + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) + + @property + def extra_authorize_data(self) -> dict: + """Extra data that needs to be appended to the authorize url.""" + return {"scope": "api"} diff --git a/homeassistant/components/ondilo_ico/const.py b/homeassistant/components/ondilo_ico/const.py new file mode 100644 index 00000000000..3c947776857 --- /dev/null +++ b/homeassistant/components/ondilo_ico/const.py @@ -0,0 +1,8 @@ +"""Constants for the Ondilo ICO integration.""" + +DOMAIN = "ondilo_ico" + +OAUTH2_AUTHORIZE = "https://interop.ondilo.com/oauth2/authorize" +OAUTH2_TOKEN = "https://interop.ondilo.com/oauth2/token" +OAUTH2_CLIENTID = "customer_api" +OAUTH2_CLIENTSECRET = "" diff --git a/homeassistant/components/ondilo_ico/manifest.json b/homeassistant/components/ondilo_ico/manifest.json new file mode 100644 index 00000000000..ee1afd315d6 --- /dev/null +++ b/homeassistant/components/ondilo_ico/manifest.json @@ -0,0 +1,15 @@ +{ + "domain": "ondilo_ico", + "name": "Ondilo ICO", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/ondilo_ico", + "requirements": [ + "ondilo==0.2.0" + ], + "dependencies": [ + "http" + ], + "codeowners": [ + "@JeromeHXP" + ] +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/oauth_impl.py b/homeassistant/components/ondilo_ico/oauth_impl.py new file mode 100644 index 00000000000..d6072cd6f6f --- /dev/null +++ b/homeassistant/components/ondilo_ico/oauth_impl.py @@ -0,0 +1,32 @@ +"""Local implementation of OAuth2 specific to Ondilo to hard code client id and secret and return a proper name.""" + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.config_entry_oauth2_flow import LocalOAuth2Implementation + +from .const import ( + DOMAIN, + OAUTH2_AUTHORIZE, + OAUTH2_CLIENTID, + OAUTH2_CLIENTSECRET, + OAUTH2_TOKEN, +) + + +class OndiloOauth2Implementation(LocalOAuth2Implementation): + """Local implementation of OAuth2 specific to Ondilo to hard code client id and secret and return a proper name.""" + + def __init__(self, hass: HomeAssistant): + """Just init default class with default values.""" + super().__init__( + hass, + DOMAIN, + OAUTH2_CLIENTID, + OAUTH2_CLIENTSECRET, + OAUTH2_AUTHORIZE, + OAUTH2_TOKEN, + ) + + @property + def name(self) -> str: + """Name of the implementation.""" + return "Ondilo" diff --git a/homeassistant/components/ondilo_ico/sensor.py b/homeassistant/components/ondilo_ico/sensor.py new file mode 100644 index 00000000000..b34ee4eae35 --- /dev/null +++ b/homeassistant/components/ondilo_ico/sensor.py @@ -0,0 +1,164 @@ +"""Platform for sensor integration.""" +from datetime import timedelta +import logging + +from ondilo import OndiloError + +from homeassistant.const import ( + CONCENTRATION_PARTS_PER_MILLION, + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, + TEMP_CELSIUS, +) +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, + UpdateFailed, +) + +from .const import DOMAIN + +SENSOR_TYPES = { + "temperature": [ + "Temperature", + TEMP_CELSIUS, + None, + DEVICE_CLASS_TEMPERATURE, + ], + "orp": ["Oxydo Reduction Potential", "mV", "mdi:pool", None], + "ph": ["pH", "", "mdi:pool", None], + "tds": ["TDS", CONCENTRATION_PARTS_PER_MILLION, "mdi:pool", None], + "battery": ["Battery", PERCENTAGE, None, DEVICE_CLASS_BATTERY], + "rssi": [ + "RSSI", + PERCENTAGE, + None, + DEVICE_CLASS_SIGNAL_STRENGTH, + ], + "salt": ["Salt", "mg/L", "mdi:pool", None], +} + +SCAN_INTERVAL = timedelta(hours=1) +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the Ondilo ICO sensors.""" + + api = hass.data[DOMAIN][entry.entry_id] + + async def async_update_data(): + """Fetch data from API endpoint. + + This is the place to pre-process the data to lookup tables + so entities can quickly look up their data. + """ + try: + return await hass.async_add_executor_job(api.get_all_pools_data) + + except OndiloError as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + # Name of the data. For logging purposes. + name="sensor", + update_method=async_update_data, + # Polling interval. Will only be polled if there are subscribers. + update_interval=SCAN_INTERVAL, + ) + + # Fetch initial data so we have data when entities subscribe + await coordinator.async_refresh() + + entities = [] + for poolidx, pool in enumerate(coordinator.data): + for sensor_idx, sensor in enumerate(pool["sensors"]): + if sensor["data_type"] in SENSOR_TYPES: + entities.append(OndiloICO(coordinator, poolidx, sensor_idx)) + + async_add_entities(entities) + + +class OndiloICO(CoordinatorEntity): + """Representation of a Sensor.""" + + def __init__( + self, coordinator: DataUpdateCoordinator, poolidx: int, sensor_idx: int + ): + """Initialize sensor entity with data from coordinator.""" + super().__init__(coordinator) + + self._poolid = self.coordinator.data[poolidx]["id"] + + pooldata = self._pooldata() + self._data_type = pooldata["sensors"][sensor_idx]["data_type"] + self._unique_id = f"{pooldata['ICO']['serial_number']}-{self._data_type}" + self._device_name = pooldata["name"] + self._name = f"{self._device_name} {SENSOR_TYPES[self._data_type][0]}" + self._device_class = SENSOR_TYPES[self._data_type][3] + self._icon = SENSOR_TYPES[self._data_type][2] + self._unit = SENSOR_TYPES[self._data_type][1] + + def _pooldata(self): + """Get pool data dict.""" + return next( + (pool for pool in self.coordinator.data if pool["id"] == self._poolid), + None, + ) + + def _devdata(self): + """Get device data dict.""" + return next( + ( + data_type + for data_type in self._pooldata()["sensors"] + if data_type["data_type"] == self._data_type + ), + None, + ) + + @property + def name(self): + """Name of the sensor.""" + return self._name + + @property + def state(self): + """Last value of the sensor.""" + return self._devdata()["value"] + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon + + @property + def device_class(self): + """Return the device class of the sensor.""" + return self._device_class + + @property + def unit_of_measurement(self): + """Return the Unit of the sensor's measurement.""" + return self._unit + + @property + def unique_id(self): + """Return the unique ID of this entity.""" + return self._unique_id + + @property + def device_info(self): + """Return the device info for the sensor.""" + pooldata = self._pooldata() + return { + "identifiers": {(DOMAIN, pooldata["ICO"]["serial_number"])}, + "name": self._device_name, + "manufacturer": "Ondilo", + "model": "ICO", + "sw_version": pooldata["ICO"]["sw_version"], + } diff --git a/homeassistant/components/ondilo_ico/strings.json b/homeassistant/components/ondilo_ico/strings.json new file mode 100644 index 00000000000..7350cc18236 --- /dev/null +++ b/homeassistant/components/ondilo_ico/strings.json @@ -0,0 +1,16 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" + } + }, + "abort": { + "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", + "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]" + }, + "create_entry": { + "default": "[%key:common::config_flow::create_entry::authenticated%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/ca.json b/homeassistant/components/ondilo_ico/translations/ca.json new file mode 100644 index 00000000000..77453bda398 --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/ca.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Temps d'espera esgotat durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", + "missing_configuration": "El component no est\u00e0 configurat. Mira'n la documentaci\u00f3." + }, + "create_entry": { + "default": "Autenticaci\u00f3 exitosa" + }, + "step": { + "pick_implementation": { + "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" + } + } + }, + "title": "Ondilo ICO" +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/cs.json b/homeassistant/components/ondilo_ico/translations/cs.json new file mode 100644 index 00000000000..bcb8849839c --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/cs.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u010casov\u00fd limit autoriza\u010dn\u00edho URL vypr\u0161el", + "missing_configuration": "Komponenta nen\u00ed nastavena. Postupujte podle dokumentace." + }, + "create_entry": { + "default": "\u00dasp\u011b\u0161n\u011b ov\u011b\u0159eno" + }, + "step": { + "pick_implementation": { + "title": "Vyberte metodu ov\u011b\u0159en\u00ed" + } + } + }, + "title": "Ondilo ICO" +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/de.json b/homeassistant/components/ondilo_ico/translations/de.json new file mode 100644 index 00000000000..5bab6ed132b --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/de.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen." + }, + "create_entry": { + "default": "Erfolgreich authentifiziert" + }, + "step": { + "pick_implementation": { + "title": "W\u00e4hle die Authentifizierungsmethode" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/en.json b/homeassistant/components/ondilo_ico/translations/en.json new file mode 100644 index 00000000000..c88a152ef81 --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/en.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Timeout generating authorize URL.", + "missing_configuration": "The component is not configured. Please follow the documentation." + }, + "create_entry": { + "default": "Successfully authenticated" + }, + "step": { + "pick_implementation": { + "title": "Pick Authentication Method" + } + } + }, + "title": "Ondilo ICO" +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/es.json b/homeassistant/components/ondilo_ico/translations/es.json new file mode 100644 index 00000000000..2394c610796 --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/es.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Tiempo de espera agotado generando la url de autorizaci\u00f3n.", + "missing_configuration": "El componente no est\u00e1 configurado. Consulta la documentaci\u00f3n." + }, + "create_entry": { + "default": "Autenticado correctamente" + }, + "step": { + "pick_implementation": { + "title": "Selecciona el m\u00e9todo de autenticaci\u00f3n" + } + } + }, + "title": "Ondilo ICO" +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/et.json b/homeassistant/components/ondilo_ico/translations/et.json new file mode 100644 index 00000000000..132e9849cf1 --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/et.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Kinnitus-URLi loomise ajal\u00f5pp", + "missing_configuration": "Komponent pole seadistatud. Palun loe dokumentatsiooni." + }, + "create_entry": { + "default": "Tuvastamine \u00f5nnestus" + }, + "step": { + "pick_implementation": { + "title": "Vali tuvastusmeetod" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/it.json b/homeassistant/components/ondilo_ico/translations/it.json new file mode 100644 index 00000000000..cd75684a437 --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/it.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Tempo scaduto nel generare l'URL di autorizzazione.", + "missing_configuration": "Il componente non \u00e8 configurato. Si prega di seguire la documentazione." + }, + "create_entry": { + "default": "Autenticazione riuscita" + }, + "step": { + "pick_implementation": { + "title": "Scegli il metodo di autenticazione" + } + } + }, + "title": "Ondilo ICO" +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/lb.json b/homeassistant/components/ondilo_ico/translations/lb.json new file mode 100644 index 00000000000..d9a5cc7482a --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/lb.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Z\u00e4itiwwerschreidung beim erstellen vun der Authorisatiouns URL.", + "missing_configuration": "Komponent net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun." + }, + "create_entry": { + "default": "Erfollegr\u00e4ich authentifiz\u00e9iert" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/no.json b/homeassistant/components/ondilo_ico/translations/no.json new file mode 100644 index 00000000000..4a06b93d045 --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/no.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Tidsavbrudd ved oppretting av godkjenningsadresse", + "missing_configuration": "Komponenten er ikke konfigurert, vennligst f\u00f8lg dokumentasjonen" + }, + "create_entry": { + "default": "Vellykket godkjenning" + }, + "step": { + "pick_implementation": { + "title": "Velg godkjenningsmetode" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/pl.json b/homeassistant/components/ondilo_ico/translations/pl.json new file mode 100644 index 00000000000..f3aa08a250f --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/pl.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "Przekroczono limit czasu generowania URL autoryzacji", + "missing_configuration": "Komponent nie jest skonfigurowany. Post\u0119puj zgodnie z dokumentacj\u0105." + }, + "create_entry": { + "default": "Pomy\u015blnie uwierzytelniono" + }, + "step": { + "pick_implementation": { + "title": "Wybierz metod\u0119 uwierzytelniania" + } + } + }, + "title": "Ondilo ICO" +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/ru.json b/homeassistant/components/ondilo_ico/translations/ru.json new file mode 100644 index 00000000000..56bb2d342b7 --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/ru.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438." + }, + "create_entry": { + "default": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u043f\u0440\u043e\u0439\u0434\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." + }, + "step": { + "pick_implementation": { + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" + } + } + }, + "title": "Ondilo ICO" +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/tr.json b/homeassistant/components/ondilo_ico/translations/tr.json new file mode 100644 index 00000000000..96722757365 --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/tr.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "Kimlik Do\u011frulama Y\u00f6ntemini Se\u00e7in" + } + } + }, + "title": "Ondilo ICO" +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/uk.json b/homeassistant/components/ondilo_ico/translations/uk.json new file mode 100644 index 00000000000..31e5834b027 --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438." + }, + "create_entry": { + "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." + }, + "step": { + "pick_implementation": { + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + } + } + }, + "title": "Ondilo ICO" +} \ No newline at end of file diff --git a/homeassistant/components/ondilo_ico/translations/zh-Hant.json b/homeassistant/components/ondilo_ico/translations/zh-Hant.json new file mode 100644 index 00000000000..ea1902b3295 --- /dev/null +++ b/homeassistant/components/ondilo_ico/translations/zh-Hant.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", + "missing_configuration": "\u5143\u4ef6\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" + }, + "create_entry": { + "default": "\u5df2\u6210\u529f\u8a8d\u8b49" + }, + "step": { + "pick_implementation": { + "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" + } + } + }, + "title": "Ondilo ICO" +} \ No newline at end of file diff --git a/homeassistant/components/onewire/sensor.py b/homeassistant/components/onewire/sensor.py index 98090dc949f..4888383fa42 100644 --- a/homeassistant/components/onewire/sensor.py +++ b/homeassistant/components/onewire/sensor.py @@ -135,6 +135,7 @@ DEVICE_SENSORS = { {"path": "counter.B", "name": "Counter B", "type": SENSOR_TYPE_COUNT}, ], "EF": [], # "HobbyBoard": special + "7E": [], # "EDS": special } DEVICE_SUPPORT_SYSBUS = ["10", "22", "28", "3B", "42"] @@ -185,6 +186,34 @@ HOBBYBOARD_EF = { ], } +# 7E sensors are special sensors by Embedded Data Systems + +EDS_SENSORS = { + "EDS0068": [ + { + "path": "EDS0068/temperature", + "name": "Temperature", + "type": SENSOR_TYPE_TEMPERATURE, + }, + { + "path": "EDS0068/pressure", + "name": "Pressure", + "type": SENSOR_TYPE_PRESSURE, + }, + { + "path": "EDS0068/light", + "name": "Illuminance", + "type": SENSOR_TYPE_ILLUMINANCE, + }, + { + "path": "EDS0068/humidity", + "name": "Humidity", + "type": SENSOR_TYPE_HUMIDITY, + }, + ], +} + + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAMES): {cv.string: cv.string}, @@ -195,12 +224,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def hb_info_from_type(dev_type="std"): +def get_sensor_types(device_sub_type): """Return the proper info array for the device type.""" - if "std" in dev_type: - return DEVICE_SENSORS - if "HobbyBoard" in dev_type: + if "HobbyBoard" in device_sub_type: return HOBBYBOARD_EF + if "EDS" in device_sub_type: + return EDS_SENSORS + return DEVICE_SENSORS async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -245,12 +275,16 @@ def get_entities(onewirehub: OneWireHub, config): family = device["family"] device_type = device["type"] device_id = os.path.split(os.path.split(device["path"])[0])[1] - dev_type = "std" + device_sub_type = "std" + device_path = device["path"] if "EF" in family: - dev_type = "HobbyBoard" + device_sub_type = "HobbyBoard" family = device_type + elif "7E" in family: + device_sub_type = "EDS" + family = onewirehub.owproxy.read(f"{device_path}device_type").decode() - if family not in hb_info_from_type(dev_type): + if family not in get_sensor_types(device_sub_type): _LOGGER.warning( "Ignoring unknown family (%s) of sensor found for device: %s", family, @@ -263,19 +297,19 @@ def get_entities(onewirehub: OneWireHub, config): "model": device_type, "name": device_id, } - for entity_specs in hb_info_from_type(dev_type)[family]: + for entity_specs in get_sensor_types(device_sub_type)[family]: if entity_specs["type"] == SENSOR_TYPE_MOISTURE: s_id = entity_specs["path"].split(".")[1] is_leaf = int( onewirehub.owproxy.read( - f"{device['path']}moisture/is_leaf.{s_id}" + f"{device_path}moisture/is_leaf.{s_id}" ).decode() ) if is_leaf: entity_specs["type"] = SENSOR_TYPE_WETNESS entity_specs["name"] = f"Wetness {s_id}" entity_path = os.path.join( - os.path.split(device["path"])[0], entity_specs["path"] + os.path.split(device_path)[0], entity_specs["path"] ) entities.append( OneWireProxySensor( diff --git a/homeassistant/components/onewire/translations/de.json b/homeassistant/components/onewire/translations/de.json index 3cc9f9cfc68..d3ed8137da3 100644 --- a/homeassistant/components/onewire/translations/de.json +++ b/homeassistant/components/onewire/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", "invalid_path": "Verzeichnis nicht gefunden." diff --git a/homeassistant/components/onewire/translations/tr.json b/homeassistant/components/onewire/translations/tr.json new file mode 100644 index 00000000000..f59da2ab7e7 --- /dev/null +++ b/homeassistant/components/onewire/translations/tr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_path": "Dizin bulunamad\u0131." + }, + "step": { + "owserver": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + } + }, + "user": { + "data": { + "type": "Ba\u011flant\u0131 t\u00fcr\u00fc" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onewire/translations/uk.json b/homeassistant/components/onewire/translations/uk.json new file mode 100644 index 00000000000..9c9705d2993 --- /dev/null +++ b/homeassistant/components/onewire/translations/uk.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_path": "\u041a\u0430\u0442\u0430\u043b\u043e\u0433 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e." + }, + "step": { + "owserver": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e owserver" + }, + "user": { + "data": { + "type": "\u0422\u0438\u043f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f" + }, + "title": "1-Wire" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onkyo/media_player.py b/homeassistant/components/onkyo/media_player.py index 2cd4faaf314..ac8cfa5e4b6 100644 --- a/homeassistant/components/onkyo/media_player.py +++ b/homeassistant/components/onkyo/media_player.py @@ -93,6 +93,9 @@ TIMEOUT_MESSAGE = "Timeout waiting for response." ATTR_HDMI_OUTPUT = "hdmi_output" ATTR_PRESET = "preset" +ATTR_AUDIO_INFORMATION = "audio_information" +ATTR_VIDEO_INFORMATION = "video_information" +ATTR_VIDEO_OUT = "video_out" ACCEPTED_VALUES = [ "no", @@ -115,6 +118,22 @@ ONKYO_SELECT_OUTPUT_SCHEMA = vol.Schema( SERVICE_SELECT_HDMI_OUTPUT = "onkyo_select_hdmi_output" +def _parse_onkyo_tuple(tup): + """Parse a tuple returned from the eiscp library.""" + if len(tup) < 2: + return None + + if isinstance(tup[1], str): + return tup[1].split(",") + + return tup[1] + + +def _tuple_get(tup, index, default=None): + """Return a tuple item at index or a default value if it doesn't exist.""" + return (tup[index : index + 1] or [default])[0] + + def determine_zones(receiver): """Determine what zones are available for the receiver.""" out = {"zone2": False, "zone3": False} @@ -229,9 +248,17 @@ class OnkyoDevice(MediaPlayerEntity): self._muted = False self._volume = 0 self._pwstate = STATE_OFF - self._name = ( - name or f"{receiver.info['model_name']}_{receiver.info['identifier']}" - ) + if name: + # not discovered + self._name = name + self._unique_id = None + else: + # discovered + self._unique_id = ( + f"{receiver.info['model_name']}_{receiver.info['identifier']}" + ) + self._name = self._unique_id + self._max_volume = max_volume self._receiver_max_volume = receiver_max_volume self._current_source = None @@ -265,6 +292,10 @@ class OnkyoDevice(MediaPlayerEntity): self._pwstate = STATE_ON else: self._pwstate = STATE_OFF + self._attributes.pop(ATTR_AUDIO_INFORMATION, None) + self._attributes.pop(ATTR_VIDEO_INFORMATION, None) + self._attributes.pop(ATTR_PRESET, None) + self._attributes.pop(ATTR_VIDEO_OUT, None) return volume_raw = self.command("volume query") @@ -278,20 +309,19 @@ class OnkyoDevice(MediaPlayerEntity): else: hdmi_out_raw = [] preset_raw = self.command("preset query") + audio_information_raw = self.command("audio-information query") + video_information_raw = self.command("video-information query") if not (volume_raw and mute_raw and current_source_raw): return - # eiscp can return string or tuple. Make everything tuples. - if isinstance(current_source_raw[1], str): - current_source_tuples = (current_source_raw[0], (current_source_raw[1],)) - else: - current_source_tuples = current_source_raw + sources = _parse_onkyo_tuple(current_source_raw) - for source in current_source_tuples[1]: + for source in sources: if source in self._source_mapping: self._current_source = self._source_mapping[source] break - self._current_source = "_".join(current_source_tuples[1]) + self._current_source = "_".join(sources) + if preset_raw and self._current_source.lower() == "radio": self._attributes[ATTR_PRESET] = preset_raw[1] elif ATTR_PRESET in self._attributes: @@ -303,12 +333,20 @@ class OnkyoDevice(MediaPlayerEntity): self._receiver_max_volume * self._max_volume / 100 ) + self._parse_audio_information(audio_information_raw) + self._parse_video_information(video_information_raw) + if not hdmi_out_raw: return - self._attributes["video_out"] = ",".join(hdmi_out_raw[1]) + self._attributes[ATTR_VIDEO_OUT] = ",".join(hdmi_out_raw[1]) if hdmi_out_raw[1] == "N/A": self._hdmi_out_supported = False + @property + def unique_id(self): + """Return unique ID for this device.""" + return self._unique_id + @property def name(self): """Return the name of the device.""" @@ -402,6 +440,37 @@ class OnkyoDevice(MediaPlayerEntity): """Set hdmi-out.""" self.command(f"hdmi-output-selector={output}") + def _parse_audio_information(self, audio_information_raw): + values = _parse_onkyo_tuple(audio_information_raw) + if values: + info = { + "format": _tuple_get(values, 1), + "input_frequency": _tuple_get(values, 2), + "input_channels": _tuple_get(values, 3), + "listening_mode": _tuple_get(values, 4), + "output_channels": _tuple_get(values, 5), + "output_frequency": _tuple_get(values, 6), + } + self._attributes[ATTR_AUDIO_INFORMATION] = info + else: + self._attributes.pop(ATTR_AUDIO_INFORMATION, None) + + def _parse_video_information(self, video_information_raw): + values = _parse_onkyo_tuple(video_information_raw) + if values: + info = { + "input_resolution": _tuple_get(values, 1), + "input_color_schema": _tuple_get(values, 2), + "input_color_depth": _tuple_get(values, 3), + "output_resolution": _tuple_get(values, 5), + "output_color_schema": _tuple_get(values, 6), + "output_color_depth": _tuple_get(values, 7), + "picture_mode": _tuple_get(values, 8), + } + self._attributes[ATTR_VIDEO_INFORMATION] = info + else: + self._attributes.pop(ATTR_VIDEO_INFORMATION, None) + class OnkyoDeviceZone(OnkyoDevice): """Representation of an Onkyo device's extra zone.""" diff --git a/homeassistant/components/onvif/__init__.py b/homeassistant/components/onvif/__init__.py index c4fbdc3f40f..b332b7a795a 100644 --- a/homeassistant/components/onvif/__init__.py +++ b/homeassistant/components/onvif/__init__.py @@ -2,7 +2,6 @@ import asyncio from onvif.exceptions import ONVIFAuthError, ONVIFError, ONVIFTimeoutError -import voluptuous as vol from homeassistant.components.ffmpeg import CONF_EXTRA_ARGUMENTS from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry @@ -33,8 +32,6 @@ from .const import ( ) from .device import ONVIFDevice -CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) - async def async_setup(hass: HomeAssistant, config: dict): """Set up the ONVIF component.""" diff --git a/homeassistant/components/onvif/translations/de.json b/homeassistant/components/onvif/translations/de.json index 25984ecf3e1..5289f6479cc 100644 --- a/homeassistant/components/onvif/translations/de.json +++ b/homeassistant/components/onvif/translations/de.json @@ -1,12 +1,15 @@ { "config": { "abort": { - "already_configured": "Das ONVIF-Ger\u00e4t ist bereits konfiguriert.", - "already_in_progress": "Der Konfigurationsfluss f\u00fcr das ONVIF-Ger\u00e4t wird bereits ausgef\u00fchrt.", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "no_h264": "Es waren keine H264-Streams verf\u00fcgbar. \u00dcberpr\u00fcfen Sie die Profilkonfiguration auf Ihrem Ger\u00e4t.", "no_mac": "Die eindeutige ID f\u00fcr das ONVIF-Ger\u00e4t konnte nicht konfiguriert werden.", "onvif_error": "Fehler beim Einrichten des ONVIF-Ger\u00e4ts. \u00dcberpr\u00fcfen Sie die Protokolle auf weitere Informationen." }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, "step": { "auth": { "data": { diff --git a/homeassistant/components/onvif/translations/tr.json b/homeassistant/components/onvif/translations/tr.json index 4e3ad18a60d..683dfbe7b92 100644 --- a/homeassistant/components/onvif/translations/tr.json +++ b/homeassistant/components/onvif/translations/tr.json @@ -1,8 +1,42 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, "step": { + "auth": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + }, + "configure_profile": { + "data": { + "include": "Kamera varl\u0131\u011f\u0131 olu\u015ftur" + }, + "title": "Profilleri Yap\u0131land\u0131r" + }, + "device": { + "data": { + "host": "Ke\u015ffedilen ONVIF cihaz\u0131n\u0131 se\u00e7in" + }, + "title": "ONVIF cihaz\u0131n\u0131 se\u00e7in" + }, + "manual_input": { + "data": { + "host": "Ana Bilgisayar", + "name": "Ad", + "port": "Port" + }, + "title": "ONVIF cihaz\u0131n\u0131 yap\u0131land\u0131r\u0131n" + }, "user": { - "description": "G\u00f6nder d\u00fc\u011fmesine t\u0131klad\u0131\u011f\u0131n\u0131zda, Profil S'yi destekleyen ONVIF cihazlar\u0131 i\u00e7in a\u011f\u0131n\u0131zda arama yapaca\u011f\u0131z. \n\n Baz\u0131 \u00fcreticiler varsay\u0131lan olarak ONVIF'i devre d\u0131\u015f\u0131 b\u0131rakmaya ba\u015flad\u0131. L\u00fctfen kameran\u0131z\u0131n yap\u0131land\u0131rmas\u0131nda ONVIF'in etkinle\u015ftirildi\u011finden emin olun." + "description": "G\u00f6nder d\u00fc\u011fmesine t\u0131klad\u0131\u011f\u0131n\u0131zda, Profil S'yi destekleyen ONVIF cihazlar\u0131 i\u00e7in a\u011f\u0131n\u0131zda arama yapaca\u011f\u0131z. \n\n Baz\u0131 \u00fcreticiler varsay\u0131lan olarak ONVIF'i devre d\u0131\u015f\u0131 b\u0131rakmaya ba\u015flad\u0131. L\u00fctfen kameran\u0131z\u0131n yap\u0131land\u0131rmas\u0131nda ONVIF'in etkinle\u015ftirildi\u011finden emin olun.", + "title": "ONVIF cihaz kurulumu" } } }, diff --git a/homeassistant/components/onvif/translations/uk.json b/homeassistant/components/onvif/translations/uk.json new file mode 100644 index 00000000000..82a816add04 --- /dev/null +++ b/homeassistant/components/onvif/translations/uk.json @@ -0,0 +1,59 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "no_h264": "\u041d\u0435\u043c\u0430\u0454 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0445 \u043f\u043e\u0442\u043e\u043a\u0456\u0432 H264. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043d\u0430 \u0441\u0432\u043e\u0454\u043c\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457.", + "no_mac": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u0443\u043d\u0456\u043a\u0430\u043b\u044c\u043d\u0438\u0439 \u0456\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0434\u043b\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e.", + "onvif_error": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u0456 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043b\u043e\u0433\u0438 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "auth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "\u0423\u0432\u0456\u0439\u0442\u0438" + }, + "configure_profile": { + "data": { + "include": "\u0421\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043e\u0431'\u0454\u043a\u0442 \u043a\u0430\u043c\u0435\u0440\u0438" + }, + "description": "\u0421\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043e\u0431'\u0454\u043a\u0442 \u043a\u0430\u043c\u0435\u0440\u0438 \u0434\u043b\u044f {profile} \u0437 \u0440\u043e\u0437\u0434\u0456\u043b\u044c\u043d\u043e\u044e \u0437\u0434\u0430\u0442\u043d\u0456\u0441\u0442\u044e {resolution}?", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u043e\u0444\u0456\u043b\u0456\u0432" + }, + "device": { + "data": { + "host": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 ONVIF" + }, + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 ONVIF" + }, + "manual_input": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e ONVIF" + }, + "user": { + "description": "\u041a\u043e\u043b\u0438 \u0412\u0438 \u043d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u041d\u0430\u0434\u0456\u0441\u043b\u0430\u0442\u0438, \u043f\u043e\u0447\u043d\u0435\u0442\u044c\u0441\u044f \u043f\u043e\u0448\u0443\u043a \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 ONVIF, \u044f\u043a\u0456 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u044e\u0442\u044c Profile S. \n\n\u0414\u0435\u044f\u043a\u0456 \u0432\u0438\u0440\u043e\u0431\u043d\u0438\u043a\u0438 \u0432 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u0445 \u0437\u0430 \u0443\u043c\u043e\u0432\u0447\u0430\u043d\u043d\u044f\u043c \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0430\u044e\u0442\u044c ONVIF. \u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e ONVIF \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e \u0432 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u0445 \u0412\u0430\u0448\u043e\u0457 \u043a\u0430\u043c\u0435\u0440\u0438.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e ONVIF" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "\u0414\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0456 \u0430\u0440\u0433\u0443\u043c\u0435\u043d\u0442\u0438 FFMPEG", + "rtsp_transport": "\u0422\u0440\u0430\u043d\u0441\u043f\u043e\u0440\u0442\u043d\u0438\u0439 \u043c\u0435\u0445\u0430\u043d\u0456\u0437\u043c RTSP" + }, + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e ONVIF" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/__init__.py b/homeassistant/components/opentherm_gw/__init__.py index 8d1d3ae4d62..cc08ec3da69 100644 --- a/homeassistant/components/opentherm_gw/__init__.py +++ b/homeassistant/components/opentherm_gw/__init__.py @@ -26,6 +26,9 @@ from homeassistant.const import ( PRECISION_WHOLE, ) import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import ( + async_get_registry as async_get_dev_reg, +) from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( @@ -404,6 +407,7 @@ class OpenThermGatewayDevice: self.gw_id = config_entry.data[CONF_ID] self.name = config_entry.data[CONF_NAME] self.climate_config = config_entry.options + self.config_entry_id = config_entry.entry_id self.status = {} self.update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_update" self.options_update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_options_update" @@ -419,9 +423,22 @@ class OpenThermGatewayDevice: async def connect_and_subscribe(self): """Connect to serial device and subscribe report handler.""" self.status = await self.gateway.connect(self.hass.loop, self.device_path) - _LOGGER.debug("Connected to OpenTherm Gateway at %s", self.device_path) - self.gw_version = self.status.get(gw_vars.OTGW_BUILD) - + version_string = self.status[gw_vars.OTGW].get(gw_vars.OTGW_ABOUT) + self.gw_version = version_string[18:] if version_string else None + _LOGGER.debug( + "Connected to OpenTherm Gateway %s at %s", self.gw_version, self.device_path + ) + dev_reg = await async_get_dev_reg(self.hass) + gw_dev = dev_reg.async_get_or_create( + config_entry_id=self.config_entry_id, + identifiers={(DOMAIN, self.gw_id)}, + name=self.name, + manufacturer="Schelte Bron", + model="OpenTherm Gateway", + sw_version=self.gw_version, + ) + if gw_dev.sw_version != self.gw_version: + dev_reg.async_update_device(gw_dev.id, sw_version=self.gw_version) self.hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, self.cleanup) async def handle_report(status): diff --git a/homeassistant/components/opentherm_gw/translations/de.json b/homeassistant/components/opentherm_gw/translations/de.json index 6e8d02bc792..36b76592945 100644 --- a/homeassistant/components/opentherm_gw/translations/de.json +++ b/homeassistant/components/opentherm_gw/translations/de.json @@ -1,7 +1,7 @@ { "config": { "error": { - "already_configured": "Gateway bereits konfiguriert", + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "cannot_connect": "Verbindung fehlgeschlagen", "id_exists": "Gateway-ID ist bereits vorhanden" }, diff --git a/homeassistant/components/opentherm_gw/translations/tr.json b/homeassistant/components/opentherm_gw/translations/tr.json new file mode 100644 index 00000000000..507b71ede5b --- /dev/null +++ b/homeassistant/components/opentherm_gw/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "init": { + "data": { + "device": "Yol veya URL" + }, + "title": "OpenTherm A\u011f Ge\u00e7idi" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/translations/uk.json b/homeassistant/components/opentherm_gw/translations/uk.json new file mode 100644 index 00000000000..af769927113 --- /dev/null +++ b/homeassistant/components/opentherm_gw/translations/uk.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "id_exists": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0448\u043b\u044e\u0437\u0443 \u0432\u0436\u0435 \u0456\u0441\u043d\u0443\u0454." + }, + "step": { + "init": { + "data": { + "device": "\u0428\u043b\u044f\u0445 \u0430\u0431\u043e URL-\u0430\u0434\u0440\u0435\u0441\u0430", + "id": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440", + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "title": "OpenTherm" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043f\u0456\u0434\u043b\u043e\u0433\u0438", + "precision": "\u0422\u043e\u0447\u043d\u0456\u0441\u0442\u044c" + }, + "description": "\u0414\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0456 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0434\u043b\u044f \u0448\u043b\u044e\u0437\u0443 Opentherm" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/translations/de.json b/homeassistant/components/openuv/translations/de.json index fae3f0f0620..88f9e69a5b6 100644 --- a/homeassistant/components/openuv/translations/de.json +++ b/homeassistant/components/openuv/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Diese Koordinaten sind bereits registriert." + "already_configured": "Standort ist bereits konfiguriert" }, "error": { "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel" diff --git a/homeassistant/components/openuv/translations/tr.json b/homeassistant/components/openuv/translations/tr.json new file mode 100644 index 00000000000..241c588f691 --- /dev/null +++ b/homeassistant/components/openuv/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "latitude": "Enlem", + "longitude": "Boylam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openuv/translations/uk.json b/homeassistant/components/openuv/translations/uk.json index fef350a3f3c..bd29fe692e1 100644 --- a/homeassistant/components/openuv/translations/uk.json +++ b/homeassistant/components/openuv/translations/uk.json @@ -1,13 +1,20 @@ { "config": { + "abort": { + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435." + }, + "error": { + "invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API" + }, "step": { "user": { "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", "elevation": "\u0412\u0438\u0441\u043e\u0442\u0430", "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430" }, - "title": "\u0417\u0430\u043f\u043e\u0432\u043d\u0456\u0442\u044c \u0432\u0430\u0448\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e" + "title": "OpenUV" } } } diff --git a/homeassistant/components/openweathermap/const.py b/homeassistant/components/openweathermap/const.py index 2eb23e23861..c70afa9cab0 100644 --- a/homeassistant/components/openweathermap/const.py +++ b/homeassistant/components/openweathermap/const.py @@ -16,6 +16,7 @@ from homeassistant.components.weather import ( ATTR_CONDITION_WINDY_VARIANT, ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRESSURE, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, @@ -92,6 +93,7 @@ MONITORED_CONDITIONS = [ FORECAST_MONITORED_CONDITIONS = [ ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRESSURE, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, @@ -210,6 +212,7 @@ WEATHER_SENSOR_TYPES = { FORECAST_SENSOR_TYPES = { ATTR_FORECAST_CONDITION: {SENSOR_NAME: "Condition"}, ATTR_FORECAST_PRECIPITATION: {SENSOR_NAME: "Precipitation"}, + ATTR_FORECAST_PRESSURE: {SENSOR_NAME: "Pressure"}, ATTR_FORECAST_TEMP: { SENSOR_NAME: "Temperature", SENSOR_UNIT: TEMP_CELSIUS, diff --git a/homeassistant/components/openweathermap/manifest.json b/homeassistant/components/openweathermap/manifest.json index 4ebdaf44c9e..e355e2e4752 100644 --- a/homeassistant/components/openweathermap/manifest.json +++ b/homeassistant/components/openweathermap/manifest.json @@ -3,6 +3,6 @@ "name": "OpenWeatherMap", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/openweathermap", - "requirements": ["pyowm==3.1.0"], + "requirements": ["pyowm==3.1.1"], "codeowners": ["@fabaff", "@freekode", "@nzapponi"] } diff --git a/homeassistant/components/openweathermap/translations/de.json b/homeassistant/components/openweathermap/translations/de.json index 239b47e2d3e..cac601b71d3 100644 --- a/homeassistant/components/openweathermap/translations/de.json +++ b/homeassistant/components/openweathermap/translations/de.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "already_configured": "Standort ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel" }, "step": { "user": { diff --git a/homeassistant/components/openweathermap/translations/tr.json b/homeassistant/components/openweathermap/translations/tr.json new file mode 100644 index 00000000000..0f845a4df73 --- /dev/null +++ b/homeassistant/components/openweathermap/translations/tr.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "latitude": "Enlem", + "longitude": "Boylam", + "mode": "Mod" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "mode": "Mod" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openweathermap/translations/uk.json b/homeassistant/components/openweathermap/translations/uk.json new file mode 100644 index 00000000000..7a39cfa078e --- /dev/null +++ b/homeassistant/components/openweathermap/translations/uk.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API" + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "language": "\u041c\u043e\u0432\u0430", + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", + "mode": "\u0420\u0435\u0436\u0438\u043c", + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 OpenWeatherMap. \u0414\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u043a\u043b\u044e\u0447\u0430 API, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u043d\u0430 https://openweathermap.org/appid.", + "title": "OpenWeatherMap" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "language": "\u041c\u043e\u0432\u0430", + "mode": "\u0420\u0435\u0436\u0438\u043c" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/openweathermap/weather_update_coordinator.py b/homeassistant/components/openweathermap/weather_update_coordinator.py index 7b127080269..93db4ca26d8 100644 --- a/homeassistant/components/openweathermap/weather_update_coordinator.py +++ b/homeassistant/components/openweathermap/weather_update_coordinator.py @@ -10,6 +10,7 @@ from homeassistant.components.weather import ( ATTR_CONDITION_SUNNY, ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, + ATTR_FORECAST_PRESSURE, ATTR_FORECAST_TEMP, ATTR_FORECAST_TEMP_LOW, ATTR_FORECAST_TIME, @@ -142,6 +143,7 @@ class WeatherUpdateCoordinator(DataUpdateCoordinator): ATTR_FORECAST_PRECIPITATION: self._calc_precipitation( entry.rain, entry.snow ), + ATTR_FORECAST_PRESSURE: entry.pressure.get("press"), ATTR_FORECAST_WIND_SPEED: entry.wind().get("speed"), ATTR_FORECAST_WIND_BEARING: entry.wind().get("deg"), ATTR_FORECAST_CONDITION: self._get_condition( diff --git a/homeassistant/components/osramlightify/manifest.json b/homeassistant/components/osramlightify/manifest.json index dfe71d4b9e1..80cfeff6e12 100644 --- a/homeassistant/components/osramlightify/manifest.json +++ b/homeassistant/components/osramlightify/manifest.json @@ -2,6 +2,6 @@ "domain": "osramlightify", "name": "Osramlightify", "documentation": "https://www.home-assistant.io/integrations/osramlightify", - "requirements": ["lightify==1.0.7.2"], + "requirements": ["lightify==1.0.7.3"], "codeowners": [] } diff --git a/homeassistant/components/ovo_energy/translations/de.json b/homeassistant/components/ovo_energy/translations/de.json index 3bd083e4839..761f6a7d247 100644 --- a/homeassistant/components/ovo_energy/translations/de.json +++ b/homeassistant/components/ovo_energy/translations/de.json @@ -1,8 +1,11 @@ { "config": { "error": { - "cannot_connect": "Verbindungsfehler" + "already_configured": "Konto wurde bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, + "flow_title": "OVO Energy: {username}", "step": { "reauth": { "data": { @@ -13,7 +16,8 @@ "data": { "password": "Passwort", "username": "Benutzername" - } + }, + "title": "Ovo Energy Account hinzuf\u00fcgen" } } } diff --git a/homeassistant/components/ovo_energy/translations/fr.json b/homeassistant/components/ovo_energy/translations/fr.json index 86719e87df4..351e20641aa 100644 --- a/homeassistant/components/ovo_energy/translations/fr.json +++ b/homeassistant/components/ovo_energy/translations/fr.json @@ -1,11 +1,16 @@ { "config": { "error": { - "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9", + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification invalide" }, "step": { + "reauth": { + "data": { + "password": "Mot de passe" + } + }, "user": { "data": { "password": "Mot de passe", diff --git a/homeassistant/components/ovo_energy/translations/lb.json b/homeassistant/components/ovo_energy/translations/lb.json index 0e007924b6a..b27b7d9702c 100644 --- a/homeassistant/components/ovo_energy/translations/lb.json +++ b/homeassistant/components/ovo_energy/translations/lb.json @@ -6,6 +6,11 @@ "invalid_auth": "Ong\u00eblteg Authentifikatioun" }, "step": { + "reauth": { + "data": { + "password": "Passwuert" + } + }, "user": { "data": { "password": "Passwuert", diff --git a/homeassistant/components/ovo_energy/translations/tr.json b/homeassistant/components/ovo_energy/translations/tr.json index f3784f6de87..714daac3253 100644 --- a/homeassistant/components/ovo_energy/translations/tr.json +++ b/homeassistant/components/ovo_energy/translations/tr.json @@ -1,5 +1,10 @@ { "config": { + "error": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, "flow_title": "OVO Enerji: {username}", "step": { "reauth": { @@ -8,6 +13,12 @@ }, "description": "OVO Energy i\u00e7in kimlik do\u011frulama ba\u015far\u0131s\u0131z oldu. L\u00fctfen mevcut kimlik bilgilerinizi girin.", "title": "Yeniden kimlik do\u011frulama" + }, + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } } } } diff --git a/homeassistant/components/ovo_energy/translations/uk.json b/homeassistant/components/ovo_energy/translations/uk.json new file mode 100644 index 00000000000..8a5f8e2a8ba --- /dev/null +++ b/homeassistant/components/ovo_energy/translations/uk.json @@ -0,0 +1,27 @@ +{ + "config": { + "error": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "flow_title": "OVO Energy: {username}", + "step": { + "reauth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457. \u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0412\u0430\u0448\u0456 \u043f\u043e\u0442\u043e\u0447\u043d\u0456 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0456 \u0434\u0430\u043d\u0456.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 OVO Energy.", + "title": "OVO Energy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/owntracks/translations/de.json b/homeassistant/components/owntracks/translations/de.json index 9d832cc264a..0bc533c0469 100644 --- a/homeassistant/components/owntracks/translations/de.json +++ b/homeassistant/components/owntracks/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, "create_entry": { "default": "\n\n\u00d6ffnen unter Android [die OwnTracks-App]({android_url}) und gehe zu {android_url} - > Verbindung. \u00c4nder die folgenden Einstellungen: \n - Modus: Privates HTTP \n - Host: {webhook_url} \n - Identifizierung: \n - Benutzername: `''` \n - Ger\u00e4te-ID: `''` \n\n\u00d6ffnen unter iOS [die OwnTracks-App]({ios_url}) und tippe auf das Symbol (i) oben links - > Einstellungen. \u00c4nder die folgenden Einstellungen: \n - Modus: HTTP \n - URL: {webhook_url} \n - Aktivieren Sie die Authentifizierung \n - UserID: `''`\n\n {secret} \n \n Weitere Informationen findest du in der [Dokumentation]({docs_url})." }, diff --git a/homeassistant/components/owntracks/translations/tr.json b/homeassistant/components/owntracks/translations/tr.json new file mode 100644 index 00000000000..a152eb19468 --- /dev/null +++ b/homeassistant/components/owntracks/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/owntracks/translations/uk.json b/homeassistant/components/owntracks/translations/uk.json index f1f31864242..e6a6fc26068 100644 --- a/homeassistant/components/owntracks/translations/uk.json +++ b/homeassistant/components/owntracks/translations/uk.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "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." + }, + "create_entry": { + "default": "\u042f\u043a\u0449\u043e \u0412\u0430\u0448 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0440\u0430\u0446\u044e\u0454 \u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0456\u0439\u043d\u0456\u0439 \u0441\u0438\u0441\u0442\u0435\u043c\u0456 Android, \u0432\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a [OwnTracks]({android_url}), \u043f\u043e\u0442\u0456\u043c preferences - > connection. \u0417\u043c\u0456\u043d\u0456\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0442\u0430\u043a, \u044f\u043a \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u043e \u043d\u0438\u0436\u0447\u0435:\n- Mode: Private HTTP\n- Host: {webhook_url}\n- Identification:\n- Username: ``\n- Device ID: `` \n\n\u042f\u043a\u0449\u043e \u0412\u0430\u0448 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0440\u0430\u0446\u044e\u0454 \u043d\u0430 iOS, \u0432\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a [OwnTracks]({ios_url}), \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c \u043d\u0430 \u0437\u043d\u0430\u0447\u043e\u043a (i) \u0432 \u043b\u0456\u0432\u043e\u043c\u0443 \u0432\u0435\u0440\u0445\u043d\u044c\u043e\u043c\u0443 \u043a\u0443\u0442\u043a\u0443 - > settings. \u0417\u043c\u0456\u043d\u0456\u0442\u044c \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0442\u0430\u043a, \u044f\u043a \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u043e \u043d\u0438\u0436\u0447\u0435:\n- Mode: HTTP\n- URL: {webhook_url}\n- Turn on authentication\n- UserID: ``\n\n{secret}\n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457." + }, "step": { "user": { "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 OwnTracks?", diff --git a/homeassistant/components/ozw/__init__.py b/homeassistant/components/ozw/__init__.py index 1f46e7a17c6..a75c05416dc 100644 --- a/homeassistant/components/ozw/__init__.py +++ b/homeassistant/components/ozw/__init__.py @@ -18,11 +18,10 @@ from openzwavemqtt.const import ( from openzwavemqtt.models.node import OZWNode from openzwavemqtt.models.value import OZWValue from openzwavemqtt.util.mqtt_client import MQTTClient -import voluptuous as vol from homeassistant.components import mqtt from homeassistant.components.hassio.handler import HassioAPIError -from homeassistant.config_entries import ConfigEntry +from homeassistant.config_entries import ENTRY_STATE_LOADED, ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady @@ -36,6 +35,7 @@ from .const import ( DATA_UNSUBSCRIBE, DOMAIN, MANAGER, + NODES_VALUES, PLATFORMS, TOPIC_OPENZWAVE, ) @@ -51,7 +51,6 @@ from .websocket_api import async_register_api _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) DATA_DEVICES = "zwave-mqtt-devices" DATA_STOP_MQTT_CLIENT = "ozw_stop_mqtt_client" @@ -68,7 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ozw_data[DATA_UNSUBSCRIBE] = [] data_nodes = {} - data_values = {} + hass.data[DOMAIN][NODES_VALUES] = data_values = {} removed_nodes = [] manager_options = {"topic_prefix": f"{TOPIC_OPENZWAVE}/"} @@ -96,12 +95,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): manager_options["send_message"] = mqtt_client.send_message else: - if "mqtt" not in hass.config.components: + mqtt_entries = hass.config_entries.async_entries("mqtt") + if not mqtt_entries or mqtt_entries[0].state != ENTRY_STATE_LOADED: _LOGGER.error("MQTT integration is not set up") return False + mqtt_entry = mqtt_entries[0] # MQTT integration only has one entry. + @callback def send_message(topic, payload): + if mqtt_entry.state != ENTRY_STATE_LOADED: + _LOGGER.error("MQTT integration is not set up") + return + mqtt.async_publish(hass, topic, json.dumps(payload)) manager_options["send_message"] = send_message @@ -348,7 +354,7 @@ async def async_handle_remove_node(hass: HomeAssistant, node: OZWNode): dev_registry = await get_dev_reg(hass) # grab device in device registry attached to this node dev_id = create_device_id(node) - device = dev_registry.async_get_device({(DOMAIN, dev_id)}, set()) + device = dev_registry.async_get_device({(DOMAIN, dev_id)}) if not device: return devices_to_remove = [device.id] @@ -372,7 +378,7 @@ async def async_handle_node_update(hass: HomeAssistant, node: OZWNode): dev_registry = await get_dev_reg(hass) # grab device in device registry attached to this node dev_id = create_device_id(node) - device = dev_registry.async_get_device({(DOMAIN, dev_id)}, set()) + device = dev_registry.async_get_device({(DOMAIN, dev_id)}) if not device: return # update device in device registry with (updated) info diff --git a/homeassistant/components/ozw/config_flow.py b/homeassistant/components/ozw/config_flow.py index 7c7c6e65dfe..00917c0609c 100644 --- a/homeassistant/components/ozw/config_flow.py +++ b/homeassistant/components/ozw/config_flow.py @@ -37,6 +37,15 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.integration_created_addon = False self.install_task = None + async def async_step_import(self, data): + """Handle imported data. + + This step will be used when importing data during zwave to ozw migration. + """ + self.network_key = data.get(CONF_NETWORK_KEY) + self.usb_path = data.get(CONF_USB_PATH) + return await self.async_step_user() + async def async_step_user(self, user_input=None): """Handle the initial step.""" if self._async_current_entries(): @@ -88,7 +97,11 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): This is the entry point for the logic that is needed when this integration will depend on the MQTT integration. """ - if "mqtt" not in self.hass.config.components: + mqtt_entries = self.hass.config_entries.async_entries("mqtt") + if ( + not mqtt_entries + or mqtt_entries[0].state != config_entries.ENTRY_STATE_LOADED + ): return self.async_abort(reason="mqtt_required") return self._async_create_entry_from_vars() @@ -163,13 +176,15 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): else: return self._async_create_entry_from_vars() - self.usb_path = self.addon_config.get(CONF_ADDON_DEVICE, "") - self.network_key = self.addon_config.get(CONF_ADDON_NETWORK_KEY, "") + usb_path = self.addon_config.get(CONF_ADDON_DEVICE, self.usb_path or "") + network_key = self.addon_config.get( + CONF_ADDON_NETWORK_KEY, self.network_key or "" + ) data_schema = vol.Schema( { - vol.Required(CONF_USB_PATH, default=self.usb_path): str, - vol.Optional(CONF_NETWORK_KEY, default=self.network_key): str, + vol.Required(CONF_USB_PATH, default=usb_path): str, + vol.Optional(CONF_NETWORK_KEY, default=network_key): str, } ) diff --git a/homeassistant/components/ozw/const.py b/homeassistant/components/ozw/const.py index f8d5090aa84..68eaf9f7c8a 100644 --- a/homeassistant/components/ozw/const.py +++ b/homeassistant/components/ozw/const.py @@ -25,6 +25,7 @@ PLATFORMS = [ SWITCH_DOMAIN, ] MANAGER = "manager" +NODES_VALUES = "nodes_values" # MQTT Topics TOPIC_OPENZWAVE = "OpenZWave" @@ -40,6 +41,9 @@ ATTR_SCENE_LABEL = "scene_label" ATTR_SCENE_VALUE_ID = "scene_value_id" ATTR_SCENE_VALUE_LABEL = "scene_value_label" +# Config entry data and options +MIGRATED = "migrated" + # Service specific SERVICE_ADD_NODE = "add_node" SERVICE_REMOVE_NODE = "remove_node" diff --git a/homeassistant/components/ozw/cover.py b/homeassistant/components/ozw/cover.py index 0ac2da91e44..1c708b55ffb 100644 --- a/homeassistant/components/ozw/cover.py +++ b/homeassistant/components/ozw/cover.py @@ -7,7 +7,6 @@ from homeassistant.components.cover import ( DOMAIN as COVER_DOMAIN, SUPPORT_CLOSE, SUPPORT_OPEN, - SUPPORT_SET_POSITION, CoverEntity, ) from homeassistant.core import callback @@ -16,9 +15,10 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import DATA_UNSUBSCRIBE, DOMAIN from .entity import ZWaveDeviceEntity -SUPPORTED_FEATURES_POSITION = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE VALUE_SELECTED_ID = "Selected_id" +PRESS_BUTTON = True +RELEASE_BUTTON = False async def async_setup_entry(hass, config_entry, async_add_entities): @@ -52,11 +52,6 @@ def percent_to_zwave_position(value): class ZWaveCoverEntity(ZWaveDeviceEntity, CoverEntity): """Representation of a Z-Wave Cover device.""" - @property - def supported_features(self): - """Flag supported features.""" - return SUPPORTED_FEATURES_POSITION - @property def is_closed(self): """Return true if cover is closed.""" @@ -73,11 +68,20 @@ class ZWaveCoverEntity(ZWaveDeviceEntity, CoverEntity): async def async_open_cover(self, **kwargs): """Open the cover.""" - self.values.primary.send_value(99) + self.values.open.send_value(PRESS_BUTTON) async def async_close_cover(self, **kwargs): """Close cover.""" - self.values.primary.send_value(0) + self.values.close.send_value(PRESS_BUTTON) + + async def async_stop_cover(self, **kwargs): + """Stop cover.""" + # Need to issue both buttons release since qt-openzwave implements idempotency + # keeping internal state of model to trigger actual updates. We could also keep + # another state in Home Assistant to know which button to release, + # but this implementation is simpler. + self.values.open.send_value(RELEASE_BUTTON) + self.values.close.send_value(RELEASE_BUTTON) class ZwaveGarageDoorBarrier(ZWaveDeviceEntity, CoverEntity): diff --git a/homeassistant/components/ozw/manifest.json b/homeassistant/components/ozw/manifest.json index a1409fd79a8..984e3f9c51a 100644 --- a/homeassistant/components/ozw/manifest.json +++ b/homeassistant/components/ozw/manifest.json @@ -7,7 +7,8 @@ "python-openzwave-mqtt[mqtt-client]==1.4.0" ], "after_dependencies": [ - "mqtt" + "mqtt", + "zwave" ], "codeowners": [ "@cgarwood", diff --git a/homeassistant/components/ozw/migration.py b/homeassistant/components/ozw/migration.py new file mode 100644 index 00000000000..86df69bc955 --- /dev/null +++ b/homeassistant/components/ozw/migration.py @@ -0,0 +1,171 @@ +"""Provide tools for migrating from the zwave integration.""" +from homeassistant.helpers.device_registry import ( + async_get_registry as async_get_device_registry, +) +from homeassistant.helpers.entity_registry import ( + async_entries_for_config_entry, + async_get_registry as async_get_entity_registry, +) + +from .const import DOMAIN, MIGRATED, NODES_VALUES +from .entity import create_device_id, create_value_id + +# The following dicts map labels between OpenZWave 1.4 and 1.6. +METER_CC_LABELS = { + "Energy": "Electric - kWh", + "Power": "Electric - W", + "Count": "Electric - Pulses", + "Voltage": "Electric - V", + "Current": "Electric - A", + "Power Factor": "Electric - PF", +} + +NOTIFICATION_CC_LABELS = { + "General": "Start", + "Smoke": "Smoke Alarm", + "Carbon Monoxide": "Carbon Monoxide", + "Carbon Dioxide": "Carbon Dioxide", + "Heat": "Heat", + "Flood": "Water", + "Access Control": "Access Control", + "Burglar": "Home Security", + "Power Management": "Power Management", + "System": "System", + "Emergency": "Emergency", + "Clock": "Clock", + "Appliance": "Appliance", + "HomeHealth": "Home Health", +} + +CC_ID_LABELS = { + 50: METER_CC_LABELS, + 113: NOTIFICATION_CC_LABELS, +} + + +async def async_get_migration_data(hass): + """Return dict with ozw side migration info.""" + data = {} + nodes_values = hass.data[DOMAIN][NODES_VALUES] + ozw_config_entries = hass.config_entries.async_entries(DOMAIN) + config_entry = ozw_config_entries[0] # ozw only has a single config entry + ent_reg = await async_get_entity_registry(hass) + entity_entries = async_entries_for_config_entry(ent_reg, config_entry.entry_id) + unique_entries = {entry.unique_id: entry for entry in entity_entries} + dev_reg = await async_get_device_registry(hass) + + for node_id, node_values in nodes_values.items(): + for entity_values in node_values: + unique_id = create_value_id(entity_values.primary) + if unique_id not in unique_entries: + continue + node = entity_values.primary.node + device_identifier = ( + DOMAIN, + create_device_id(node, entity_values.primary.instance), + ) + device_entry = dev_reg.async_get_device({device_identifier}, set()) + data[unique_id] = { + "node_id": node_id, + "node_instance": entity_values.primary.instance, + "device_id": device_entry.id, + "command_class": entity_values.primary.command_class.value, + "command_class_label": entity_values.primary.label, + "value_index": entity_values.primary.index, + "unique_id": unique_id, + "entity_entry": unique_entries[unique_id], + } + + return data + + +def map_node_values(zwave_data, ozw_data): + """Map zwave node values onto ozw node values.""" + migration_map = {"device_entries": {}, "entity_entries": {}} + + for zwave_entry in zwave_data.values(): + node_id = zwave_entry["node_id"] + node_instance = zwave_entry["node_instance"] + cc_id = zwave_entry["command_class"] + zwave_cc_label = zwave_entry["command_class_label"] + + if cc_id in CC_ID_LABELS: + labels = CC_ID_LABELS[cc_id] + ozw_cc_label = labels.get(zwave_cc_label, zwave_cc_label) + + ozw_entry = next( + ( + entry + for entry in ozw_data.values() + if entry["node_id"] == node_id + and entry["node_instance"] == node_instance + and entry["command_class"] == cc_id + and entry["command_class_label"] == ozw_cc_label + ), + None, + ) + else: + value_index = zwave_entry["value_index"] + + ozw_entry = next( + ( + entry + for entry in ozw_data.values() + if entry["node_id"] == node_id + and entry["node_instance"] == node_instance + and entry["command_class"] == cc_id + and entry["value_index"] == value_index + ), + None, + ) + + if ozw_entry is None: + continue + + # Save the zwave_entry under the ozw entity_id to create the map. + # Check that the mapped entities have the same domain. + if zwave_entry["entity_entry"].domain == ozw_entry["entity_entry"].domain: + migration_map["entity_entries"][ + ozw_entry["entity_entry"].entity_id + ] = zwave_entry + migration_map["device_entries"][ozw_entry["device_id"]] = zwave_entry[ + "device_id" + ] + + return migration_map + + +async def async_migrate(hass, migration_map): + """Perform zwave to ozw migration.""" + dev_reg = await async_get_device_registry(hass) + for ozw_device_id, zwave_device_id in migration_map["device_entries"].items(): + zwave_device_entry = dev_reg.async_get(zwave_device_id) + dev_reg.async_update_device( + ozw_device_id, + area_id=zwave_device_entry.area_id, + name_by_user=zwave_device_entry.name_by_user, + ) + + ent_reg = await async_get_entity_registry(hass) + for zwave_entry in migration_map["entity_entries"].values(): + zwave_entity_id = zwave_entry["entity_entry"].entity_id + ent_reg.async_remove(zwave_entity_id) + + for ozw_entity_id, zwave_entry in migration_map["entity_entries"].items(): + entity_entry = zwave_entry["entity_entry"] + ent_reg.async_update_entity( + ozw_entity_id, + new_entity_id=entity_entry.entity_id, + name=entity_entry.name, + icon=entity_entry.icon, + ) + + zwave_config_entry = hass.config_entries.async_entries("zwave")[0] + await hass.config_entries.async_remove(zwave_config_entry.entry_id) + + ozw_config_entry = hass.config_entries.async_entries("ozw")[0] + updates = { + **ozw_config_entry.data, + MIGRATED: True, + } + hass.config_entries.async_update_entry(ozw_config_entry, data=updates) diff --git a/homeassistant/components/ozw/translations/ca.json b/homeassistant/components/ozw/translations/ca.json index 4553589e36d..835c16eb449 100644 --- a/homeassistant/components/ozw/translations/ca.json +++ b/homeassistant/components/ozw/translations/ca.json @@ -26,7 +26,7 @@ "data": { "use_addon": "Utilitza el complement OpenZWave Supervisor" }, - "description": "Voleu utilitzar el complement OpenZWave Supervisor?", + "description": "Vols utilitzar el complement Supervisor d'OpenZWave?", "title": "Selecciona el m\u00e8tode de connexi\u00f3" }, "start_addon": { diff --git a/homeassistant/components/ozw/translations/de.json b/homeassistant/components/ozw/translations/de.json index 70eaaaf18df..afa26fb7e03 100644 --- a/homeassistant/components/ozw/translations/de.json +++ b/homeassistant/components/ozw/translations/de.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", - "mqtt_required": "Die MQTT-Integration ist nicht eingerichtet" + "mqtt_required": "Die MQTT-Integration ist nicht eingerichtet", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "progress": { "install_addon": "Bitte warten, bis die Installation des OpenZWave-Add-Ons abgeschlossen ist. Dies kann einige Minuten dauern." @@ -14,6 +15,15 @@ }, "install_addon": { "title": "Die Installation des OpenZWave-Add-On wurde gestartet" + }, + "on_supervisor": { + "title": "Verbindungstyp ausw\u00e4hlen" + }, + "start_addon": { + "data": { + "network_key": "Netzwerk-Schl\u00fcssel", + "usb_path": "USB-Ger\u00e4te-Pfad" + } } } } diff --git a/homeassistant/components/ozw/translations/lb.json b/homeassistant/components/ozw/translations/lb.json index f97f026d38b..33de9a44953 100644 --- a/homeassistant/components/ozw/translations/lb.json +++ b/homeassistant/components/ozw/translations/lb.json @@ -1,8 +1,17 @@ { "config": { "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert", + "already_in_progress": "Konfiguratioun's Oflaf ass schon am gaang", "mqtt_required": "MQTT Integratioun ass net ageriicht", "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech." + }, + "step": { + "start_addon": { + "data": { + "network_key": "Netzwierk Schl\u00ebssel" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/ozw/translations/no.json b/homeassistant/components/ozw/translations/no.json index 89563ff3533..652e28fe3fc 100644 --- a/homeassistant/components/ozw/translations/no.json +++ b/homeassistant/components/ozw/translations/no.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "addon_info_failed": "Kunne ikke hente OpenZWave-tilleggsinfo", - "addon_install_failed": "Kunne ikke installere OpenZWave-tillegget", + "addon_info_failed": "Kunne ikke hente informasjon om OpenZWave-tillegg", + "addon_install_failed": "Kunne ikke installere OpenZWave-tillegg", "addon_set_config_failed": "Kunne ikke angi OpenZWave-konfigurasjon", "already_configured": "Enheten er allerede konfigurert", "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", @@ -10,23 +10,23 @@ "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig." }, "error": { - "addon_start_failed": "Kunne ikke starte OpenZWave-tillegget. Sjekk konfigurasjonen." + "addon_start_failed": "Kunne ikke starte OpenZWave-tillegg. Sjekk konfigurasjonen." }, "progress": { - "install_addon": "Vent mens OpenZWave-tilleggsinstallasjonen er ferdig. Dette kan ta flere minutter." + "install_addon": "Vent mens installasjonen av OpenZWave-tillegg er ferdig. Dette kan ta flere minutter." }, "step": { "hassio_confirm": { - "title": "Sett opp OpenZWave-integrasjon med OpenZWave-tillegget" + "title": "Sett opp OpenZWave-integrasjon med OpenZWave-tillegg" }, "install_addon": { - "title": "Installasjonen av tilleggsprogrammet OpenZWave har startet" + "title": "Installasjonen av OpenZWave-tillegg har startet" }, "on_supervisor": { "data": { - "use_addon": "Bruk OpenZWave Supervisor-tillegget" + "use_addon": "Bruk OpenZWave Supervisor-tillegg" }, - "description": "\u00d8nsker du \u00e5 bruke OpenZWave Supervisor-tillegget?", + "description": "\u00d8nsker du \u00e5 bruke OpenZWave Supervisor-tillegg?", "title": "Velg tilkoblingsmetode" }, "start_addon": { @@ -34,7 +34,7 @@ "network_key": "Nettverksn\u00f8kkel", "usb_path": "USB enhetsbane" }, - "title": "Angi OpenZWave-tilleggskonfigurasjonen" + "title": "Angi konfigurasjon for OpenZWave-tillegg" } } } diff --git a/homeassistant/components/ozw/translations/tr.json b/homeassistant/components/ozw/translations/tr.json index d0a70d57752..99eda8b8311 100644 --- a/homeassistant/components/ozw/translations/tr.json +++ b/homeassistant/components/ozw/translations/tr.json @@ -2,7 +2,12 @@ "config": { "abort": { "addon_info_failed": "OpenZWave eklenti bilgileri al\u0131namad\u0131.", - "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + "addon_install_failed": "OpenZWave eklentisi y\u00fcklenemedi.", + "addon_set_config_failed": "OpenZWave yap\u0131land\u0131rmas\u0131 ayarlanamad\u0131.", + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "mqtt_required": "MQTT entegrasyonu kurulmam\u0131\u015f", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." }, "progress": { "install_addon": "OpenZWave eklenti kurulumu bitene kadar l\u00fctfen bekleyin. Bu birka\u00e7 dakika s\u00fcrebilir." @@ -10,6 +15,18 @@ "step": { "install_addon": { "title": "OpenZWave eklenti kurulumu ba\u015flad\u0131" + }, + "on_supervisor": { + "data": { + "use_addon": "OpenZWave Supervisor eklentisini kullan\u0131n" + }, + "description": "OpenZWave Supervisor eklentisini kullanmak istiyor musunuz?", + "title": "Ba\u011flant\u0131 y\u00f6ntemini se\u00e7in" + }, + "start_addon": { + "data": { + "network_key": "A\u011f Anahtar\u0131" + } } } } diff --git a/homeassistant/components/ozw/translations/uk.json b/homeassistant/components/ozw/translations/uk.json new file mode 100644 index 00000000000..f8fb161aa1c --- /dev/null +++ b/homeassistant/components/ozw/translations/uk.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "addon_info_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u0434\u043e\u043f\u043e\u0432\u043d\u0435\u043d\u043d\u044f OpenZWave.", + "addon_install_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0438 OpenZWave.", + "addon_set_config_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u0438 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e OpenZWave.", + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "mqtt_required": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f MQTT \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0430.", + "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." + }, + "error": { + "addon_start_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0438 OpenZWave. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e." + }, + "progress": { + "install_addon": "\u0417\u0430\u0447\u0435\u043a\u0430\u0439\u0442\u0435, \u043f\u043e\u043a\u0438 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c\u0441\u044f \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0434\u043e\u043f\u043e\u0432\u043d\u0435\u043d\u043d\u044f OpenZWave. \u0426\u0435 \u043c\u043e\u0436\u0435 \u0437\u0430\u0439\u043d\u044f\u0442\u0438 \u043a\u0456\u043b\u044c\u043a\u0430 \u0445\u0432\u0438\u043b\u0438\u043d." + }, + "step": { + "hassio_confirm": { + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u0434\u043e\u043f\u043e\u0432\u043d\u0435\u043d\u043d\u044f OpenZWave" + }, + "install_addon": { + "title": "\u0420\u043e\u0437\u043f\u043e\u0447\u0430\u0442\u043e \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0434\u043e\u043f\u043e\u0432\u043d\u0435\u043d\u043d\u044f Open'Wave" + }, + "on_supervisor": { + "data": { + "use_addon": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u043e\u043a Supervisor OpenZWave" + }, + "description": "\u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0434\u043e\u0434\u0430\u0442\u043e\u043a Supervisor OpenZWave?", + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f" + }, + "start_addon": { + "data": { + "network_key": "\u041a\u043b\u044e\u0447 \u043c\u0435\u0440\u0435\u0436\u0456", + "usb_path": "\u0428\u043b\u044f\u0445 \u0434\u043e USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + }, + "title": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u043e\u0433\u043e \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 Open'Wave" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ozw/websocket_api.py b/homeassistant/components/ozw/websocket_api.py index 3ee6e040743..708b9045b57 100644 --- a/homeassistant/components/ozw/websocket_api.py +++ b/homeassistant/components/ozw/websocket_api.py @@ -1,4 +1,6 @@ """Web socket API for OpenZWave.""" +import logging + from openzwavemqtt.const import ( ATTR_CODE_SLOT, ATTR_LABEL, @@ -23,7 +25,11 @@ from homeassistant.helpers import config_validation as cv from .const import ATTR_CONFIG_PARAMETER, ATTR_CONFIG_VALUE, DOMAIN, MANAGER from .lock import ATTR_USERCODE +from .migration import async_get_migration_data, async_migrate, map_node_values +_LOGGER = logging.getLogger(__name__) + +DRY_RUN = "dry_run" TYPE = "type" ID = "id" OZW_INSTANCE = "ozw_instance" @@ -52,6 +58,7 @@ ATTR_NEIGHBORS = "neighbors" @callback def async_register_api(hass): """Register all of our api endpoints.""" + websocket_api.async_register_command(hass, websocket_migrate_zwave) websocket_api.async_register_command(hass, websocket_get_instances) websocket_api.async_register_command(hass, websocket_get_nodes) websocket_api.async_register_command(hass, websocket_network_status) @@ -161,6 +168,63 @@ def _get_config_params(node, *args): return config_params +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(TYPE): "ozw/migrate_zwave", + vol.Optional(DRY_RUN, default=True): bool, + } +) +async def websocket_migrate_zwave(hass, connection, msg): + """Migrate the zwave integration device and entity data to ozw integration.""" + if "zwave" not in hass.config.components: + _LOGGER.error("Can not migrate, zwave integration is not loaded") + connection.send_message( + websocket_api.error_message( + msg["id"], "zwave_not_loaded", "Integration zwave is not loaded" + ) + ) + return + + zwave = hass.components.zwave + zwave_data = await zwave.async_get_ozw_migration_data(hass) + _LOGGER.debug("Migration zwave data: %s", zwave_data) + + ozw_data = await async_get_migration_data(hass) + _LOGGER.debug("Migration ozw data: %s", ozw_data) + + can_migrate = map_node_values(zwave_data, ozw_data) + + zwave_entity_ids = [ + entry["entity_entry"].entity_id for entry in zwave_data.values() + ] + ozw_entity_ids = [entry["entity_entry"].entity_id for entry in ozw_data.values()] + migration_device_map = { + zwave_device_id: ozw_device_id + for ozw_device_id, zwave_device_id in can_migrate["device_entries"].items() + } + migration_entity_map = { + zwave_entry["entity_entry"].entity_id: ozw_entity_id + for ozw_entity_id, zwave_entry in can_migrate["entity_entries"].items() + } + _LOGGER.debug("Migration entity map: %s", migration_entity_map) + + if not msg[DRY_RUN]: + await async_migrate(hass, can_migrate) + + connection.send_result( + msg[ID], + { + "migration_device_map": migration_device_map, + "zwave_entity_ids": zwave_entity_ids, + "ozw_entity_ids": ozw_entity_ids, + "migration_entity_map": migration_entity_map, + "migrated": not msg[DRY_RUN], + }, + ) + + @websocket_api.websocket_command({vol.Required(TYPE): "ozw/get_instances"}) def websocket_get_instances(hass, connection, msg): """Get a list of OZW instances.""" diff --git a/homeassistant/components/panasonic_viera/__init__.py b/homeassistant/components/panasonic_viera/__init__.py index f51c6d9d372..3305c935890 100644 --- a/homeassistant/components/panasonic_viera/__init__.py +++ b/homeassistant/components/panasonic_viera/__init__.py @@ -66,7 +66,6 @@ async def async_setup(hass, config): async def async_setup_entry(hass, config_entry): """Set up Panasonic Viera from a config entry.""" - panasonic_viera_data = hass.data.setdefault(DOMAIN, {}) config = config_entry.data @@ -162,7 +161,6 @@ class Remote: async def async_create_remote_control(self, during_setup=False): """Create remote control.""" - control_existed = self._control is not None try: params = {} if self._app_id and self._encryption_key: @@ -173,21 +171,18 @@ class Remote: partial(RemoteControl, self._host, self._port, **params) ) - self.state = STATE_ON - self.available = True + if during_setup: + await self.async_update() except (TimeoutError, URLError, SOAPError, OSError) as err: - if control_existed or during_setup: - _LOGGER.debug("Could not establish remote connection: %s", err) - + _LOGGER.debug("Could not establish remote connection: %s", err) self._control = None self.state = STATE_OFF self.available = self._on_action is not None except Exception as err: # pylint: disable=broad-except - if control_existed or during_setup: - _LOGGER.exception("An unknown error occurred: %s", err) - self._control = None - self.state = STATE_OFF - self.available = self._on_action is not None + _LOGGER.exception("An unknown error occurred: %s", err) + self._control = None + self.state = STATE_OFF + self.available = self._on_action is not None async def async_update(self): """Update device data.""" @@ -215,16 +210,15 @@ class Remote: """Turn on the TV.""" if self._on_action is not None: await self._on_action.async_run(context=context) - self.state = STATE_ON + await self.async_update() elif self.state != STATE_ON: await self.async_send_key(Keys.power) - self.state = STATE_ON + await self.async_update() async def async_turn_off(self): """Turn off the TV.""" if self.state != STATE_OFF: await self.async_send_key(Keys.power) - self.state = STATE_OFF await self.async_update() async def async_set_mute(self, enable): @@ -252,12 +246,20 @@ class Remote: async def _handle_errors(self, func, *args): """Handle errors from func, set available and reconnect if needed.""" try: - return await self._hass.async_add_executor_job(func, *args) + result = await self._hass.async_add_executor_job(func, *args) + self.state = STATE_ON + self.available = True + return result except EncryptionRequired: _LOGGER.error( "The connection couldn't be encrypted. Please reconfigure your TV" ) - except (TimeoutError, URLError, SOAPError, OSError): + self.available = False + except (SOAPError): + self.state = STATE_OFF + self.available = True + await self.async_create_remote_control() + except (TimeoutError, URLError, OSError): self.state = STATE_OFF self.available = self._on_action is not None await self.async_create_remote_control() diff --git a/homeassistant/components/panasonic_viera/translations/de.json b/homeassistant/components/panasonic_viera/translations/de.json index 4b2c14be9d6..71090830714 100644 --- a/homeassistant/components/panasonic_viera/translations/de.json +++ b/homeassistant/components/panasonic_viera/translations/de.json @@ -1,20 +1,20 @@ { "config": { "abort": { - "already_configured": "Dieser Panasonic Viera TV ist bereits konfiguriert.", - "cannot_connect": "Verbindungsfehler", - "unknown": "Ein unbekannter Fehler ist aufgetreten. Weitere Informationen finden Sie in den Logs." + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" }, "error": { - "cannot_connect": "Verbindungsfehler", - "invalid_pin_code": "Der von Ihnen eingegebene PIN-Code war ung\u00fcltig" + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_pin_code": "Der eingegebene PIN-Code war ung\u00fcltig" }, "step": { "pairing": { "data": { - "pin": "PIN" + "pin": "PIN-Code" }, - "description": "Geben Sie die auf Ihrem Fernseher angezeigte PIN ein", + "description": "Gib den auf deinem TV angezeigten PIN-Code ein", "title": "Kopplung" }, "user": { @@ -22,7 +22,7 @@ "host": "IP-Adresse", "name": "Name" }, - "description": "Geben Sie die IP-Adresse Ihres Panasonic Viera TV ein", + "description": "Gib die IP-Adresse deines Panasonic Viera TV ein", "title": "Richten Sie Ihr Fernsehger\u00e4t ein" } } diff --git a/homeassistant/components/panasonic_viera/translations/tr.json b/homeassistant/components/panasonic_viera/translations/tr.json new file mode 100644 index 00000000000..d0e573fdcf9 --- /dev/null +++ b/homeassistant/components/panasonic_viera/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "\u0130p Adresi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/uk.json b/homeassistant/components/panasonic_viera/translations/uk.json new file mode 100644 index 00000000000..9722b19ece9 --- /dev/null +++ b/homeassistant/components/panasonic_viera/translations/uk.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_pin_code": "\u0412\u0432\u0435\u0434\u0435\u043d\u0438\u0439 PIN-\u043a\u043e\u0434 \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439." + }, + "step": { + "pairing": { + "data": { + "pin": "PIN-\u043a\u043e\u0434" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c PIN-\u043a\u043e\u0434 , \u0449\u043e \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u0454\u0442\u044c\u0441\u044f \u043d\u0430 \u0435\u043a\u0440\u0430\u043d\u0456 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430", + "title": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f" + }, + "user": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441\u0430 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 Panasonic Viera", + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 49a9f4c3bea..5f08f79dc00 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -6,6 +6,7 @@ from typing import Any, Mapping, MutableMapping, Optional import voluptuous as vol from homeassistant.components import websocket_api +from homeassistant.const import ATTR_FRIENDLY_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import TemplateError from homeassistant.helpers import config_validation as cv @@ -125,6 +126,7 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: title = title.template attr[ATTR_TITLE] = title + attr[ATTR_FRIENDLY_NAME] = title try: message.hass = hass diff --git a/homeassistant/components/persistent_notification/services.yaml b/homeassistant/components/persistent_notification/services.yaml index d026896a7c5..7917a06a3d7 100644 --- a/homeassistant/components/persistent_notification/services.yaml +++ b/homeassistant/components/persistent_notification/services.yaml @@ -8,7 +8,7 @@ create: description: Optional title for your notification. [Optional, Templates accepted] example: Test notification notification_id: - description: Target ID of the notification, will replace a notification with the same Id. [Optional] + description: Target ID of the notification, will replace a notification with the same ID. [Optional] example: 1234 dismiss: diff --git a/homeassistant/components/person/translations/uk.json b/homeassistant/components/person/translations/uk.json index 0dba7914da0..5e6b186e38c 100644 --- a/homeassistant/components/person/translations/uk.json +++ b/homeassistant/components/person/translations/uk.json @@ -2,7 +2,7 @@ "state": { "_": { "home": "\u0412\u0434\u043e\u043c\u0430", - "not_home": "\u0412\u0456\u0434\u0441\u0443\u0442\u043d\u0456\u0439" + "not_home": "\u041d\u0435 \u0432\u0434\u043e\u043c\u0430" } }, "title": "\u041b\u044e\u0434\u0438\u043d\u0430" diff --git a/homeassistant/components/pi_hole/__init__.py b/homeassistant/components/pi_hole/__init__.py index c9b7937da73..2d540d936e5 100644 --- a/homeassistant/components/pi_hole/__init__.py +++ b/homeassistant/components/pi_hole/__init__.py @@ -26,6 +26,7 @@ from homeassistant.helpers.update_coordinator import ( from .const import ( CONF_LOCATION, + CONF_STATISTICS_ONLY, DATA_KEY_API, DATA_KEY_COORDINATOR, DEFAULT_LOCATION, @@ -83,6 +84,12 @@ async def async_setup_entry(hass, entry): location = entry.data[CONF_LOCATION] api_key = entry.data.get(CONF_API_KEY) + # For backward compatibility + if CONF_STATISTICS_ONLY not in entry.data: + hass.config_entries.async_update_entry( + entry, data={**entry.data, CONF_STATISTICS_ONLY: not api_key} + ) + _LOGGER.debug("Setting up %s integration with host %s", DOMAIN, host) try: @@ -146,7 +153,7 @@ async def async_unload_entry(hass, entry): def _async_platforms(entry): """Return platforms to be loaded / unloaded.""" platforms = ["sensor"] - if entry.data.get(CONF_API_KEY): + if not entry.data[CONF_STATISTICS_ONLY]: platforms.append("switch") else: platforms.append("binary_sensor") diff --git a/homeassistant/components/pi_hole/config_flow.py b/homeassistant/components/pi_hole/config_flow.py index c7061b05caa..a7d4b387b1c 100644 --- a/homeassistant/components/pi_hole/config_flow.py +++ b/homeassistant/components/pi_hole/config_flow.py @@ -8,9 +8,11 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.pi_hole.const import ( # pylint: disable=unused-import CONF_LOCATION, + CONF_STATISTICS_ONLY, DEFAULT_LOCATION, DEFAULT_NAME, DEFAULT_SSL, + DEFAULT_STATISTICS_ONLY, DEFAULT_VERIFY_SSL, DOMAIN, ) @@ -33,6 +35,10 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + def __init__(self): + """Initialize the config flow.""" + self._config = None + async def async_step_user(self, user_input=None): """Handle a flow initiated by the user.""" return await self.async_step_init(user_input) @@ -55,67 +61,93 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): location = user_input[CONF_LOCATION] tls = user_input[CONF_SSL] verify_tls = user_input[CONF_VERIFY_SSL] - api_token = user_input.get(CONF_API_KEY) endpoint = f"{host}/{location}" if await self._async_endpoint_existed(endpoint): return self.async_abort(reason="already_configured") try: - await self._async_try_connect( - host, location, tls, verify_tls, api_token - ) - return self.async_create_entry( - title=name, - data={ - CONF_HOST: host, - CONF_NAME: name, - CONF_LOCATION: location, - CONF_SSL: tls, - CONF_VERIFY_SSL: verify_tls, - CONF_API_KEY: api_token, - }, - ) + await self._async_try_connect(host, location, tls, verify_tls) except HoleError as ex: _LOGGER.debug("Connection failed: %s", ex) if is_import: _LOGGER.error("Failed to import: %s", ex) return self.async_abort(reason="cannot_connect") errors["base"] = "cannot_connect" + else: + self._config = { + CONF_HOST: host, + CONF_NAME: name, + CONF_LOCATION: location, + CONF_SSL: tls, + CONF_VERIFY_SSL: verify_tls, + } + if is_import: + api_key = user_input.get(CONF_API_KEY) + return self.async_create_entry( + title=name, + data={ + **self._config, + CONF_STATISTICS_ONLY: api_key is None, + CONF_API_KEY: api_key, + }, + ) + self._config[CONF_STATISTICS_ONLY] = user_input[CONF_STATISTICS_ONLY] + if self._config[CONF_STATISTICS_ONLY]: + return self.async_create_entry(title=name, data=self._config) + return await self.async_step_api_key() user_input = user_input or {} return self.async_show_form( step_id="user", data_schema=vol.Schema( { + vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str, vol.Required( - CONF_HOST, default=user_input.get(CONF_HOST) or "" - ): str, - vol.Required( - CONF_PORT, default=user_input.get(CONF_PORT) or 80 + CONF_PORT, default=user_input.get(CONF_PORT, 80) ): vol.Coerce(int), vol.Required( - CONF_NAME, default=user_input.get(CONF_NAME) or DEFAULT_NAME + CONF_NAME, default=user_input.get(CONF_NAME, DEFAULT_NAME) ): str, vol.Required( CONF_LOCATION, - default=user_input.get(CONF_LOCATION) or DEFAULT_LOCATION, - ): str, - vol.Optional( - CONF_API_KEY, default=user_input.get(CONF_API_KEY) or "" + default=user_input.get(CONF_LOCATION, DEFAULT_LOCATION), ): str, vol.Required( - CONF_SSL, default=user_input.get(CONF_SSL) or DEFAULT_SSL + CONF_STATISTICS_ONLY, + default=user_input.get( + CONF_STATISTICS_ONLY, DEFAULT_STATISTICS_ONLY + ), + ): bool, + vol.Required( + CONF_SSL, + default=user_input.get(CONF_SSL, DEFAULT_SSL), ): bool, vol.Required( CONF_VERIFY_SSL, - default=user_input.get(CONF_VERIFY_SSL) or DEFAULT_VERIFY_SSL, + default=user_input.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL), ): bool, } ), errors=errors, ) + async def async_step_api_key(self, user_input=None): + """Handle step to setup API key.""" + if user_input is not None: + return self.async_create_entry( + title=self._config[CONF_NAME], + data={ + **self._config, + CONF_API_KEY: user_input.get(CONF_API_KEY, ""), + }, + ) + + return self.async_show_form( + step_id="api_key", + data_schema=vol.Schema({vol.Optional(CONF_API_KEY): str}), + ) + async def _async_endpoint_existed(self, endpoint): existing_endpoints = [ f"{entry.data.get(CONF_HOST)}/{entry.data.get(CONF_LOCATION)}" @@ -123,14 +155,7 @@ class PiHoleFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ] return endpoint in existing_endpoints - async def _async_try_connect(self, host, location, tls, verify_tls, api_token): + async def _async_try_connect(self, host, location, tls, verify_tls): session = async_get_clientsession(self.hass, verify_tls) - pi_hole = Hole( - host, - self.hass.loop, - session, - location=location, - tls=tls, - api_token=api_token, - ) + pi_hole = Hole(host, self.hass.loop, session, location=location, tls=tls) await pi_hole.get_data() diff --git a/homeassistant/components/pi_hole/const.py b/homeassistant/components/pi_hole/const.py index b15db5f3980..f1871bf27c8 100644 --- a/homeassistant/components/pi_hole/const.py +++ b/homeassistant/components/pi_hole/const.py @@ -6,12 +6,14 @@ from homeassistant.const import PERCENTAGE DOMAIN = "pi_hole" CONF_LOCATION = "location" +CONF_STATISTICS_ONLY = "statistics_only" DEFAULT_LOCATION = "admin" DEFAULT_METHOD = "GET" DEFAULT_NAME = "Pi-Hole" DEFAULT_SSL = False DEFAULT_VERIFY_SSL = True +DEFAULT_STATISTICS_ONLY = True SERVICE_DISABLE = "disable" SERVICE_DISABLE_ATTR_DURATION = "duration" diff --git a/homeassistant/components/pi_hole/strings.json b/homeassistant/components/pi_hole/strings.json index 75af03dc3a5..fbf3c5a627b 100644 --- a/homeassistant/components/pi_hole/strings.json +++ b/homeassistant/components/pi_hole/strings.json @@ -8,9 +8,15 @@ "name": "[%key:common::config_flow::data::name%]", "location": "[%key:common::config_flow::data::location%]", "api_key": "[%key:common::config_flow::data::api_key%]", + "statistics_only": "Statistics Only", "ssl": "[%key:common::config_flow::data::ssl%]", "verify_ssl": "[%key:common::config_flow::data::verify_ssl%]" } + }, + "api_key": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" + } } }, "error": { diff --git a/homeassistant/components/pi_hole/translations/ca.json b/homeassistant/components/pi_hole/translations/ca.json index 37d4e890ef4..eb15fa7bf97 100644 --- a/homeassistant/components/pi_hole/translations/ca.json +++ b/homeassistant/components/pi_hole/translations/ca.json @@ -7,6 +7,11 @@ "cannot_connect": "Ha fallat la connexi\u00f3" }, "step": { + "api_key": { + "data": { + "api_key": "Clau API" + } + }, "user": { "data": { "api_key": "Clau API", @@ -15,6 +20,7 @@ "name": "Nom", "port": "Port", "ssl": "Utilitza un certificat SSL", + "statistics_only": "Nom\u00e9s les estad\u00edstiques", "verify_ssl": "Verifica el certificat SSL" } } diff --git a/homeassistant/components/pi_hole/translations/cs.json b/homeassistant/components/pi_hole/translations/cs.json index a9057ceabab..fa90fbdb2a0 100644 --- a/homeassistant/components/pi_hole/translations/cs.json +++ b/homeassistant/components/pi_hole/translations/cs.json @@ -7,6 +7,11 @@ "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, "step": { + "api_key": { + "data": { + "api_key": "Kl\u00ed\u010d API" + } + }, "user": { "data": { "api_key": "Kl\u00ed\u010d API", diff --git a/homeassistant/components/pi_hole/translations/de.json b/homeassistant/components/pi_hole/translations/de.json index f74c5acb635..34198fcfebe 100644 --- a/homeassistant/components/pi_hole/translations/de.json +++ b/homeassistant/components/pi_hole/translations/de.json @@ -4,17 +4,17 @@ "already_configured": "Service ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung konnte nicht hergestellt werden" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "user": { "data": { - "api_key": "API-Schl\u00fcssel (optional)", + "api_key": "API-Schl\u00fcssel", "host": "Host", "location": "Org", "name": "Name", "port": "Port", - "ssl": "SSL verwenden", + "ssl": "Nutzt ein SSL-Zertifikat", "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" } } diff --git a/homeassistant/components/pi_hole/translations/en.json b/homeassistant/components/pi_hole/translations/en.json index 858e7c230ac..9053a70c18f 100644 --- a/homeassistant/components/pi_hole/translations/en.json +++ b/homeassistant/components/pi_hole/translations/en.json @@ -7,6 +7,11 @@ "cannot_connect": "Failed to connect" }, "step": { + "api_key": { + "data": { + "api_key": "API Key" + } + }, "user": { "data": { "api_key": "API Key", @@ -15,6 +20,7 @@ "name": "Name", "port": "Port", "ssl": "Uses an SSL certificate", + "statistics_only": "Statistics Only", "verify_ssl": "Verify SSL certificate" } } diff --git a/homeassistant/components/pi_hole/translations/es.json b/homeassistant/components/pi_hole/translations/es.json index 48708d68104..35597af49f2 100644 --- a/homeassistant/components/pi_hole/translations/es.json +++ b/homeassistant/components/pi_hole/translations/es.json @@ -7,6 +7,11 @@ "cannot_connect": "No se pudo conectar" }, "step": { + "api_key": { + "data": { + "api_key": "Clave API" + } + }, "user": { "data": { "api_key": "Clave API", @@ -15,6 +20,7 @@ "name": "Nombre", "port": "Puerto", "ssl": "Usar SSL", + "statistics_only": "S\u00f3lo las estad\u00edsticas", "verify_ssl": "Verificar certificado SSL" } } diff --git a/homeassistant/components/pi_hole/translations/et.json b/homeassistant/components/pi_hole/translations/et.json index c68d52c0c10..4ff0fdd0ba8 100644 --- a/homeassistant/components/pi_hole/translations/et.json +++ b/homeassistant/components/pi_hole/translations/et.json @@ -7,6 +7,11 @@ "cannot_connect": "\u00dchendamine nurjus" }, "step": { + "api_key": { + "data": { + "api_key": "API v\u00f5ti" + } + }, "user": { "data": { "api_key": "API v\u00f5ti", @@ -15,6 +20,7 @@ "name": "Nimi", "port": "", "ssl": "Kasuatb SSL serti", + "statistics_only": "Ainult statistika", "verify_ssl": "Kontrolli SSL sertifikaati" } } diff --git a/homeassistant/components/pi_hole/translations/it.json b/homeassistant/components/pi_hole/translations/it.json index 34590ee77bb..7d355caf985 100644 --- a/homeassistant/components/pi_hole/translations/it.json +++ b/homeassistant/components/pi_hole/translations/it.json @@ -7,6 +7,11 @@ "cannot_connect": "Impossibile connettersi" }, "step": { + "api_key": { + "data": { + "api_key": "Chiave API" + } + }, "user": { "data": { "api_key": "Chiave API", @@ -15,6 +20,7 @@ "name": "Nome", "port": "Porta", "ssl": "Utilizza un certificato SSL", + "statistics_only": "Solo Statistiche", "verify_ssl": "Verificare il certificato SSL" } } diff --git a/homeassistant/components/pi_hole/translations/no.json b/homeassistant/components/pi_hole/translations/no.json index 71c815ecd36..7d005fa6516 100644 --- a/homeassistant/components/pi_hole/translations/no.json +++ b/homeassistant/components/pi_hole/translations/no.json @@ -7,6 +7,11 @@ "cannot_connect": "Tilkobling mislyktes" }, "step": { + "api_key": { + "data": { + "api_key": "API-n\u00f8kkel" + } + }, "user": { "data": { "api_key": "API-n\u00f8kkel", @@ -15,6 +20,7 @@ "name": "Navn", "port": "Port", "ssl": "Bruker et SSL-sertifikat", + "statistics_only": "Bare statistikk", "verify_ssl": "Verifisere SSL-sertifikat" } } diff --git a/homeassistant/components/pi_hole/translations/pl.json b/homeassistant/components/pi_hole/translations/pl.json index add788ef916..ee4b6eadd87 100644 --- a/homeassistant/components/pi_hole/translations/pl.json +++ b/homeassistant/components/pi_hole/translations/pl.json @@ -7,6 +7,11 @@ "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, "step": { + "api_key": { + "data": { + "api_key": "Klucz API" + } + }, "user": { "data": { "api_key": "Klucz API", @@ -15,6 +20,7 @@ "name": "Nazwa", "port": "Port", "ssl": "Certyfikat SSL", + "statistics_only": "Tylko statystyki", "verify_ssl": "Weryfikacja certyfikatu SSL" } } diff --git a/homeassistant/components/pi_hole/translations/ru.json b/homeassistant/components/pi_hole/translations/ru.json index eb3cfa62c62..eed9596c907 100644 --- a/homeassistant/components/pi_hole/translations/ru.json +++ b/homeassistant/components/pi_hole/translations/ru.json @@ -7,6 +7,11 @@ "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, "step": { + "api_key": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + } + }, "user": { "data": { "api_key": "\u041a\u043b\u044e\u0447 API", @@ -15,6 +20,7 @@ "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", "port": "\u041f\u043e\u0440\u0442", "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL", + "statistics_only": "\u0422\u043e\u043b\u044c\u043a\u043e \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0430", "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" } } diff --git a/homeassistant/components/pi_hole/translations/tr.json b/homeassistant/components/pi_hole/translations/tr.json new file mode 100644 index 00000000000..a14e020d360 --- /dev/null +++ b/homeassistant/components/pi_hole/translations/tr.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "api_key": { + "data": { + "api_key": "API Anahtar\u0131" + } + }, + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "host": "Ana Bilgisayar", + "location": "Konum", + "port": "Port", + "statistics_only": "Yaln\u0131zca \u0130statistikler" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/uk.json b/homeassistant/components/pi_hole/translations/uk.json new file mode 100644 index 00000000000..93413f9abff --- /dev/null +++ b/homeassistant/components/pi_hole/translations/uk.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "host": "\u0425\u043e\u0441\u0442", + "location": "\u041c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f", + "name": "\u041d\u0430\u0437\u0432\u0430", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL", + "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pi_hole/translations/zh-Hant.json b/homeassistant/components/pi_hole/translations/zh-Hant.json index 1cea5a87f4b..1527b48f580 100644 --- a/homeassistant/components/pi_hole/translations/zh-Hant.json +++ b/homeassistant/components/pi_hole/translations/zh-Hant.json @@ -7,6 +7,11 @@ "cannot_connect": "\u9023\u7dda\u5931\u6557" }, "step": { + "api_key": { + "data": { + "api_key": "API \u5bc6\u9470" + } + }, "user": { "data": { "api_key": "API \u5bc6\u9470", @@ -15,6 +20,7 @@ "name": "\u540d\u7a31", "port": "\u901a\u8a0a\u57e0", "ssl": "\u4f7f\u7528 SSL \u8a8d\u8b49", + "statistics_only": "\u50c5\u7d71\u8a08\u8cc7\u8a0a", "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" } } diff --git a/homeassistant/components/plaato/translations/ca.json b/homeassistant/components/plaato/translations/ca.json index 1dbe125d50d..c4669b219ab 100644 --- a/homeassistant/components/plaato/translations/ca.json +++ b/homeassistant/components/plaato/translations/ca.json @@ -1,16 +1,53 @@ { "config": { "abort": { + "already_configured": "El compte ja ha estat configurat", "single_instance_allowed": "Ja configurat. Nom\u00e9s \u00e9s possible una sola configuraci\u00f3.", "webhook_not_internet_accessible": "La teva inst\u00e0ncia de Home Assistant ha de ser accessible des d'Internet per poder rebre missatges webhook." }, "create_entry": { - "default": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar l'opci\u00f3 webhook de Plaato Airlock.\n\nCompleta la seg\u00fcent informaci\u00f3:\n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n\nConsulta la [documentaci\u00f3]({docs_url}) per a m\u00e9s detalls." + "default": "El dispositiu Plaato {device_type} amb nom **{device_name}** s'ha configurat correctament!" + }, + "error": { + "invalid_webhook_device": "Has seleccionat un dispositiu que no admet l'enviament de dades a un webhook. Nom\u00e9s est\u00e0 disponible per a Airlock", + "no_api_method": "Has d'afegir un token d'autenticaci\u00f3 o seleccionar webhook", + "no_auth_token": "Has d'afegir un token d'autenticaci\u00f3" }, "step": { + "api_method": { + "data": { + "token": "Enganxa el token d'autenticaci\u00f3 aqu\u00ed", + "use_webhook": "Utilitza webhook" + }, + "description": "Per poder consultar l'API, cal un `auth_token` que es pot obtenir seguint aquestes [instruccions](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token)\n\n Dispositiu seleccionat: **{device_type}** \n\n Si prefereixes utilitzar el m\u00e8tode webhook integrat (nom\u00e9s per Airlock), marca la casella seg\u00fcent i deixa el token d'autenticaci\u00f3 en blanc", + "title": "Selecciona el m\u00e8tode API" + }, "user": { + "data": { + "device_name": "Posa un nom al dispositiu", + "device_type": "Tipus de dispositiu Plaato" + }, "description": "Vols comen\u00e7ar la configuraci\u00f3?", - "title": "Configuraci\u00f3 del Webhook de Plaato" + "title": "Configura dispositius Plaato" + }, + "webhook": { + "description": "Per enviar esdeveniments a Home Assistant, haur\u00e0s de configurar la opci\u00f3 webhook de Plaato Airlock.\n\nCompleta la seg\u00fcent informaci\u00f3:\n\n- URL: `{webhook_url}` \n- M\u00e8tode: POST \n\nConsulta la [documentaci\u00f3]({docs_url}) per a m\u00e9s detalls.", + "title": "Webhook a utilitzar" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "update_interval": "Interval d'actualitzaci\u00f3 (minuts)" + }, + "description": "Estableix l'interval d'actualitzaci\u00f3 (minuts)", + "title": "Opcions de Plaato" + }, + "webhook": { + "description": "Informaci\u00f3 del webhook: \n\n - URL: `{webhook_url}`\n - M\u00e8tode: POST\n\n", + "title": "Opcions de Plaato Airlock" } } } diff --git a/homeassistant/components/plaato/translations/de.json b/homeassistant/components/plaato/translations/de.json index f97fe4875f7..5171baab654 100644 --- a/homeassistant/components/plaato/translations/de.json +++ b/homeassistant/components/plaato/translations/de.json @@ -1,11 +1,15 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", + "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." + }, "create_entry": { "default": "Um Ereignisse an Home Assistant zu senden, muss das Webhook Feature in Plaato Airlock konfiguriert werden.\n\n F\u00fcge die folgenden Informationen ein: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n \n Weitere Informationen finden sich in der [Dokumentation]({docs_url})." }, "step": { "user": { - "description": "Soll Plaato Airlock wirklich eingerichtet werden?", + "description": "M\u00f6chtest du mit der Einrichtung beginnen?", "title": "Plaato Webhook einrichten" } } diff --git a/homeassistant/components/plaato/translations/et.json b/homeassistant/components/plaato/translations/et.json index 75c7a2182ef..ec7b7e4b1a4 100644 --- a/homeassistant/components/plaato/translations/et.json +++ b/homeassistant/components/plaato/translations/et.json @@ -1,16 +1,53 @@ { "config": { "abort": { + "already_configured": "Kasutaja on juba seadistatud", "single_instance_allowed": "Juba seadistatud. V\u00f5imalik on ainult \u00fcks seadistamine.", "webhook_not_internet_accessible": "Veebikonksu s\u00f5numite vastuv\u00f5tmiseks peab Home Assistant olema Interneti kaudu juurdep\u00e4\u00e4setav." }, "create_entry": { - "default": "S\u00fcndmuste saatmiseks Home Assistantile pead seadistama Plaatoo Airlock'i veebihaagi. \n\n Sisesta j\u00e4rgmine teave: \n\n - URL: \" {webhook_url} \" \n - Meetod: POST \n \n Lisateavet leiad [documentation] ( {docs_url} )." + "default": "{device_type} Plaato seade nimega **{device_name}** on edukalt seadistatud!" + }, + "error": { + "invalid_webhook_device": "Oled valinud seadme, mis ei toeta andmete saatmist veebihaagile. See on saadaval ainult Airlocki jaoks", + "no_api_method": "Pead lisama autentimisloa v\u00f5i valima veebihaagi", + "no_auth_token": "Pead lisama autentimisloa" }, "step": { + "api_method": { + "data": { + "token": "Aseta Auth Token siia", + "use_webhook": "Kasuta veebihaaki" + }, + "description": "API p\u00e4ringu esitamiseks on vajalik \"auth_token\", mille saad j\u00e4rgides [neid] (https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) juhiseid \n\n Valitud seade: ** {device_type} ** \n\n Kui kasutad pigem sisseehitatud veebihaagi meetodit (ainult Airlock), m\u00e4rgi palun allolev ruut ja j\u00e4ta Auth Token t\u00fchjaks", + "title": "Vali API meetod" + }, "user": { + "data": { + "device_name": "Pang oma seadmele nimi", + "device_type": "Plaato seadme t\u00fc\u00fcp" + }, "description": "Kas alustan seadistamist?", - "title": "Plaato Webhooki seadistamine" + "title": "Plaato seadmete h\u00e4\u00e4lestamine" + }, + "webhook": { + "description": "S\u00fcndmuste saatmiseks Home Assistanti pead seadistama Plaato Airlocki veebihaagi. \n\n Sisesta j\u00e4rgmine teave: \n\n - URL: \" {webhook_url} \"\n - Meetod: POST \n\n Lisateavet leiad [dokumentatsioonist] ( {docs_url} ).", + "title": "Kasutatav veebihaak" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "update_interval": "V\u00e4rskendamise intervall (minutites)" + }, + "description": "M\u00e4\u00e4ra v\u00e4rskendamise intervall (minutites)", + "title": "Plaato valikud" + }, + "webhook": { + "description": "Veebihaagi teave: \n\n - URL: `{webhook_url}`\n - Meetod: POST\n\n", + "title": "Plaato Airlocki valikud" } } } diff --git a/homeassistant/components/plaato/translations/no.json b/homeassistant/components/plaato/translations/no.json index 1e2da1bfb12..8873399aaa4 100644 --- a/homeassistant/components/plaato/translations/no.json +++ b/homeassistant/components/plaato/translations/no.json @@ -1,16 +1,48 @@ { "config": { "abort": { + "already_configured": "Kontoen er allerede konfigurert", "single_instance_allowed": "Allerede konfigurert. Bare \u00e9n enkelt konfigurasjon er mulig.", "webhook_not_internet_accessible": "Home Assistant forekomsten din m\u00e5 v\u00e6re tilgjengelig fra internett for \u00e5 kunne motta webhook meldinger" }, "create_entry": { "default": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp webhook-funksjonen i Plaato Airlock. \n\n Fyll ut f\u00f8lgende informasjon: \n\n - URL: `{webhook_url}` \n - Metode: POST \n\n Se [dokumentasjonen]({docs_url}) for ytterligere detaljer." }, + "error": { + "invalid_webhook_device": "Du har valgt en enhet som ikke st\u00f8tter sending av data til en webhook. Den er kun tilgjengelig for Airlock", + "no_api_method": "Du m\u00e5 legge til et godkjenningstoken eller velge webhook", + "no_auth_token": "Du m\u00e5 legge til et godkjenningstoken" + }, "step": { + "api_method": { + "title": "Velg API-metode" + }, "user": { + "data": { + "device_name": "Navngi enheten din", + "device_type": "Type Platon-enhet" + }, "description": "Vil du starte oppsettet?", "title": "Sett opp Plaato Webhook" + }, + "webhook": { + "description": "For \u00e5 sende hendelser til Home Assistant, m\u00e5 du sette opp webhook-funksjonen i Plaato Airlock. \n\n Fyll ut f\u00f8lgende informasjon: \n\n - URL: `{webhook_url}` \n - Metode: POST \n\n Se [dokumentasjonen]({docs_url}) for ytterligere detaljer.", + "title": "Webhook \u00e5 bruke" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "update_interval": "Oppdateringsintervall (minutter)" + }, + "description": "Still inn oppdateringsintervallet (minutter)", + "title": "Alternativer for Plaato" + }, + "webhook": { + "description": "Webhook info:\n\n- URL-adresse: {webhook_url}\n- Metode: POST\n\n", + "title": "Alternativer for Plaato Airlock" } } } diff --git a/homeassistant/components/plaato/translations/pl.json b/homeassistant/components/plaato/translations/pl.json index 1f7c8141aa5..c849f574c9c 100644 --- a/homeassistant/components/plaato/translations/pl.json +++ b/homeassistant/components/plaato/translations/pl.json @@ -1,16 +1,53 @@ { "config": { "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane", "single_instance_allowed": "Ju\u017c skonfigurowano. Mo\u017cliwa jest tylko jedna konfiguracja.", "webhook_not_internet_accessible": "Tw\u00f3j Home Assistant musi by\u0107 dost\u0119pny z Internetu, aby odbiera\u0107 komunikaty webhook" }, "create_entry": { - "default": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistanta, musisz skonfigurowa\u0107 webhook w Plaato Airlock. \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y." + "default": "Tw\u00f3j {device_type} Plaato o nazwie *{device_name}* zosta\u0142o pomy\u015blnie skonfigurowane!" + }, + "error": { + "invalid_webhook_device": "Wybra\u0142e\u015b urz\u0105dzenie, kt\u00f3re nie obs\u0142uguje wysy\u0142ania danych do webhooka. Opcja dost\u0119pna tylko w areometrze Airlock", + "no_api_method": "Musisz doda\u0107 token uwierzytelniania lub wybra\u0107 webhook", + "no_auth_token": "Musisz doda\u0107 token autoryzacji" }, "step": { + "api_method": { + "data": { + "token": "Wklej token autoryzacji", + "use_webhook": "U\u017cyj webhook" + }, + "description": "Aby m\u00f3c przesy\u0142a\u0107 zapytania do API, wymagany jest \u201etoken autoryzacji\u201d, kt\u00f3ry mo\u017cna uzyska\u0107, post\u0119puj\u0105c zgodnie z [t\u0105] (https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) instrukcj\u0105\n\nWybrane urz\u0105dzenie: *{device_type}* \n\nJe\u015bli wolisz u\u017cywa\u0107 wbudowanej metody webhook (tylko areomierz Airlock), zaznacz poni\u017csze pole i pozostaw token autoryzacji pusty", + "title": "Wybierz metod\u0119 API" + }, "user": { + "data": { + "device_name": "Nazwij swoje urz\u0105dzenie", + "device_type": "Rodzaj urz\u0105dzenia Plaato" + }, "description": "Czy chcesz rozpocz\u0105\u0107 konfiguracj\u0119?", - "title": "Konfiguracja Plaato Webhook" + "title": "Konfiguracja urz\u0105dze\u0144 Plaato" + }, + "webhook": { + "description": "Aby wysy\u0142a\u0107 zdarzenia do Home Assistanta, musisz skonfigurowa\u0107 webhook w Plaato Airlock. \n\n Wprowad\u017a nast\u0119puj\u0105ce dane:\n\n - URL: `{webhook_url}` \n - Metoda: POST \n\nZapoznaj si\u0119 z [dokumentacj\u0105]({docs_url}), by pozna\u0107 szczeg\u00f3\u0142y.", + "title": "Webhook do u\u017cycia" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "update_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji (w minutach)" + }, + "description": "Ustaw cz\u0119stotliwo\u015bci aktualizacji (w minutach)", + "title": "Opcje dla Plaato" + }, + "webhook": { + "description": "Informacje o webhook: \n\n - URL: `{webhook_url}`\n - Metoda: POST \n\n", + "title": "Opcje dla areomierza Plaato Airlock" } } } diff --git a/homeassistant/components/plaato/translations/tr.json b/homeassistant/components/plaato/translations/tr.json new file mode 100644 index 00000000000..1f21b08ec81 --- /dev/null +++ b/homeassistant/components/plaato/translations/tr.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." + }, + "error": { + "no_auth_token": "Bir kimlik do\u011frulama jetonu eklemeniz gerekiyor" + }, + "step": { + "api_method": { + "data": { + "use_webhook": "Webhook kullan" + }, + "title": "API y\u00f6ntemini se\u00e7in" + }, + "user": { + "data": { + "device_name": "Cihaz\u0131n\u0131z\u0131 adland\u0131r\u0131n", + "device_type": "Plaato cihaz\u0131n\u0131n t\u00fcr\u00fc" + } + }, + "webhook": { + "title": "Webhook kullanmak i\u00e7in" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "update_interval": "G\u00fcncelle\u015ftirme aral\u0131\u011f\u0131 (dakika)" + }, + "description": "G\u00fcncelleme aral\u0131\u011f\u0131n\u0131 ayarlay\u0131n (dakika)", + "title": "Plaato i\u00e7in se\u00e7enekler" + }, + "webhook": { + "title": "Plaato Airlock i\u00e7in se\u00e7enekler" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/translations/uk.json b/homeassistant/components/plaato/translations/uk.json new file mode 100644 index 00000000000..a4f7de7c6be --- /dev/null +++ b/homeassistant/components/plaato/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "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.", + "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c." + }, + "create_entry": { + "default": "\u0414\u043b\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043a\u0438 \u043f\u043e\u0434\u0456\u0439 \u0432 Home Assistant \u0432\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Webhook \u0434\u043b\u044f Plaato Airlock. \n\n\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e: \n\n- URL: `{webhook_url}`\n- Method: POST \n\n \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457." + }, + "step": { + "user": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?", + "title": "Plaato Airlock" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plaato/translations/zh-Hant.json b/homeassistant/components/plaato/translations/zh-Hant.json index aec745ea38b..2890c5c31c6 100644 --- a/homeassistant/components/plaato/translations/zh-Hant.json +++ b/homeassistant/components/plaato/translations/zh-Hant.json @@ -1,16 +1,53 @@ { "config": { "abort": { + "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "single_instance_allowed": "\u50c5\u80fd\u8a2d\u5b9a\u4e00\u7d44\u88dd\u7f6e\u3002", "webhook_not_internet_accessible": "Home Assistant \u5be6\u9ad4\u5fc5\u9808\u8981\u80fd\u5f9e\u7db2\u969b\u7db2\u8def\u5b58\u53d6\u65b9\u80fd\u63a5\u6536 Webhook \u8a0a\u606f\u3002" }, "create_entry": { - "default": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc Plaato Airlock \u5167\u8a2d\u5b9a webhook \u529f\u80fd\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002" + "default": "\u540d\u7a31\u70ba **{device_name}** \u7684 Plaato {device_type} \u5df2\u6210\u529f\u8a2d\u5b9a\uff01" + }, + "error": { + "invalid_webhook_device": "\u6240\u9078\u64c7\u7684\u88dd\u7f6e\u4e0d\u652f\u63f4\u50b3\u9001\u8cc7\u6599\u81f3 Webhook\u3001AirLock \u50c5\u652f\u63f4\u6b64\u985e\u578b", + "no_api_method": "\u9700\u8981\u65b0\u589e\u6388\u6b0a\u5bc6\u9470\u6216\u9078\u64c7 Webhook", + "no_auth_token": "\u9700\u8981\u65b0\u589e\u6388\u6b0a\u5bc6\u9470" }, "step": { + "api_method": { + "data": { + "token": "\u65bc\u6b64\u8cbc\u4e0a\u6388\u6b0a\u5bc6\u9470", + "use_webhook": "\u4f7f\u7528 Webhook" + }, + "description": "\u9700\u8981\u6388\u6b0a\u5bc6\u8981 `auth_token` \u65b9\u80fd\u67e5\u8a62 API\u3002\u7372\u5f97\u7684\u65b9\u6cd5\u8acb [\u53c3\u95b1](https://plaato.zendesk.com/hc/en-us/articles/360003234717-Auth-token) \u6559\u5b78\n\n\u9078\u64c7\u7684\u88dd\u7f6e\uff1a**{device_type}** \n\n\u5047\u5982\u9078\u64c7\u5167\u5efa Webhook \u65b9\u6cd5\uff08Airlock \u552f\u4e00\u652f\u63f4\uff09\uff0c\u8acb\u6aa2\u67e5\u4e0b\u65b9\u6838\u9078\u76d2\u4e26\u78ba\u5b9a\u4fdd\u6301\u6388\u6b0a\u5bc6\u9470\u6b04\u4f4d\u7a7a\u767d", + "title": "\u9078\u64c7 API \u65b9\u5f0f" + }, "user": { + "data": { + "device_name": "\u88dd\u7f6e\u540d\u7a31", + "device_type": "Plaato \u88dd\u7f6e\u985e\u578b" + }, "description": "\u662f\u5426\u8981\u958b\u59cb\u8a2d\u5b9a\uff1f", - "title": "\u8a2d\u5b9a Plaato Webhook" + "title": "\u8a2d\u5b9a Plaato \u88dd\u7f6e" + }, + "webhook": { + "description": "\u6b32\u50b3\u9001\u4e8b\u4ef6\u81f3 Home Assistant\uff0c\u5c07\u9700\u65bc Plaato Airlock \u5167\u8a2d\u5b9a webhook \u529f\u80fd\u3002\n\n\u8acb\u586b\u5beb\u4e0b\u5217\u8cc7\u8a0a\uff1a\n\n- URL: `{webhook_url}`\n- Method: POST\n\n\u8acb\u53c3\u95b1 [\u6587\u4ef6]({docs_url})\u4ee5\u4e86\u89e3\u66f4\u8a73\u7d30\u8cc7\u6599\u3002", + "title": "\u4f7f\u7528\u4e4b Webhook" + } + } + }, + "options": { + "step": { + "user": { + "data": { + "update_interval": "\u66f4\u65b0\u983b\u7387\uff08\u5206\uff09" + }, + "description": "\u8a2d\u5b9a\u66f4\u65b0\u983b\u7387\uff08\u5206\uff09", + "title": "Plaato \u9078\u9805" + }, + "webhook": { + "description": "Webhook \u8a0a\u606f\uff1a\n\n- URL\uff1a`{webhook_url}`\n- \u65b9\u5f0f\uff1aPOST\n\n", + "title": "Plaato Airlock \u9078\u9805" } } } diff --git a/homeassistant/components/plant/translations/uk.json b/homeassistant/components/plant/translations/uk.json index 3204c42a714..25f24b43b80 100644 --- a/homeassistant/components/plant/translations/uk.json +++ b/homeassistant/components/plant/translations/uk.json @@ -1,8 +1,8 @@ { "state": { "_": { - "ok": "\u0422\u0410\u041a", - "problem": "\u0425\u0430\u043b\u0435\u043f\u0430" + "ok": "\u041e\u041a", + "problem": "\u041f\u0440\u043e\u0431\u043b\u0435\u043c\u0430" } }, "title": "\u0420\u043e\u0441\u043b\u0438\u043d\u0430" diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index de8b278f3cf..6b403150e9c 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -14,24 +14,17 @@ from plexwebsocket import ( PlexWebsocket, ) import requests.exceptions -import voluptuous as vol from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.components.media_player.const import ( - ATTR_MEDIA_CONTENT_ID, - ATTR_MEDIA_CONTENT_TYPE, -) from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY, SOURCE_REAUTH from homeassistant.const import ( - ATTR_ENTITY_ID, CONF_SOURCE, CONF_URL, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import callback -from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError -from homeassistant.helpers import config_validation as cv +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.debounce import Debouncer from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -48,12 +41,11 @@ from .const import ( PLEX_SERVER_CONFIG, PLEX_UPDATE_PLATFORMS_SIGNAL, SERVERS, - SERVICE_PLAY_ON_SONOS, WEBSOCKETS, ) from .errors import ShouldUpdateConfigEntry from .server import PlexServer -from .services import async_setup_services, lookup_plex_media +from .services import async_setup_services _LOGGER = logging.getLogger(__package__) @@ -218,31 +210,13 @@ async def async_setup_entry(hass, entry): ) task.add_done_callback(partial(start_websocket_session, platform)) - async def async_play_on_sonos_service(service_call): - await hass.async_add_executor_job(play_on_sonos, hass, service_call) - - play_on_sonos_schema = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_id, - vol.Required(ATTR_MEDIA_CONTENT_ID): cv.string, - vol.Optional(ATTR_MEDIA_CONTENT_TYPE): vol.In("music"), - } - ) - def get_plex_account(plex_server): try: return plex_server.account except (plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized): return None - plex_account = await hass.async_add_executor_job(get_plex_account, plex_server) - if plex_account: - hass.services.async_register( - PLEX_DOMAIN, - SERVICE_PLAY_ON_SONOS, - async_play_on_sonos_service, - schema=play_on_sonos_schema, - ) + await hass.async_add_executor_job(get_plex_account, plex_server) return True @@ -276,30 +250,3 @@ async def async_options_updated(hass, entry): # Guard incomplete setup during reauth flows if server_id in hass.data[PLEX_DOMAIN][SERVERS]: hass.data[PLEX_DOMAIN][SERVERS][server_id].options = entry.options - - -def play_on_sonos(hass, service_call): - """Play Plex media on a linked Sonos device.""" - entity_id = service_call.data[ATTR_ENTITY_ID] - content_id = service_call.data[ATTR_MEDIA_CONTENT_ID] - content_type = service_call.data.get(ATTR_MEDIA_CONTENT_TYPE) - - sonos = hass.components.sonos - try: - sonos_name = sonos.get_coordinator_name(entity_id) - except HomeAssistantError as err: - _LOGGER.error("Cannot get Sonos device: %s", err) - return - - media, plex_server = lookup_plex_media(hass, content_type, content_id) - if media is None: - return - - sonos_speaker = plex_server.account.sonos_speaker(sonos_name) - if sonos_speaker is None: - _LOGGER.error( - "Sonos speaker '%s' could not be found on this Plex account", sonos_name - ) - return - - sonos_speaker.playMedia(media) diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index c13be439be7..eec433202e4 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -47,7 +47,6 @@ X_PLEX_VERSION = __version__ AUTOMATIC_SETUP_STRING = "Obtain a new token from plex.tv" MANUAL_SETUP_STRING = "Configure Plex server manually" -SERVICE_PLAY_ON_SONOS = "play_on_sonos" SERVICE_REFRESH_LIBRARY = "refresh_library" SERVICE_SCAN_CLIENTS = "scan_for_clients" diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index 5bfbc4932ab..913f405cfcd 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -4,11 +4,10 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/plex", "requirements": [ - "plexapi==4.2.0", + "plexapi==4.3.1", "plexauth==0.0.6", "plexwebsocket==0.0.12" ], "dependencies": ["http"], - "after_dependencies": ["sonos"], "codeowners": ["@jjlawren"] } diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 24e37216b70..1a57186bd9b 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -22,6 +22,7 @@ from homeassistant.components.media_player.const import ( ) from homeassistant.const import STATE_IDLE, STATE_PAUSED, STATE_PLAYING from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -495,16 +496,26 @@ class PlexMediaPlayer(MediaPlayerEntity): if isinstance(src, int): src = {"plex_key": src} - shuffle = src.pop("shuffle", 0) - media = self.plex_server.lookup_media(media_type, **src) + playqueue_id = src.pop("playqueue_id", None) - if media is None: - _LOGGER.error("Media could not be found: %s", media_id) - return + if playqueue_id: + try: + playqueue = self.plex_server.get_playqueue(playqueue_id) + except plexapi.exceptions.NotFound as err: + raise HomeAssistantError( + f"PlayQueue '{playqueue_id}' could not be found" + ) from err + else: + shuffle = src.pop("shuffle", 0) + media = self.plex_server.lookup_media(media_type, **src) - _LOGGER.debug("Attempting to play %s on %s", media, self.name) + if media is None: + _LOGGER.error("Media could not be found: %s", media_id) + return + + _LOGGER.debug("Attempting to play %s on %s", media, self.name) + playqueue = self.plex_server.create_playqueue(media, shuffle=shuffle) - playqueue = self.plex_server.create_playqueue(media, shuffle=shuffle) try: self.device.playMedia(playqueue) except requests.exceptions.ConnectTimeout: diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 3834833b740..1baceb78ff1 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -146,7 +146,7 @@ class PlexServer: available_servers = [ (x.name, x.clientIdentifier) for x in self.account.resources() - if "server" in x.provides + if "server" in x.provides and x.presence ] if not available_servers: @@ -593,6 +593,10 @@ class PlexServer: """Create playqueue on Plex server.""" return plexapi.playqueue.PlayQueue.create(self._plex_server, media, **kwargs) + def get_playqueue(self, playqueue_id): + """Retrieve existing playqueue from Plex server.""" + return plexapi.playqueue.PlayQueue.get(self._plex_server, playqueue_id) + def fetch_item(self, item): """Fetch item from Plex server.""" return self._plex_server.fetchItem(item) diff --git a/homeassistant/components/plex/services.py b/homeassistant/components/plex/services.py index 19a04a61a9f..a5faa56a8bb 100644 --- a/homeassistant/components/plex/services.py +++ b/homeassistant/components/plex/services.py @@ -5,6 +5,7 @@ import logging from plexapi.exceptions import NotFound import voluptuous as vol +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.dispatcher import async_dispatcher_send from .const import ( @@ -52,8 +53,6 @@ def refresh_library(hass, service_call): library_name = service_call.data["library_name"] plex_server = get_plex_server(hass, plex_server_name) - if not plex_server: - return try: library = plex_server.library.section(title=library_name) @@ -71,7 +70,11 @@ def refresh_library(hass, service_call): def get_plex_server(hass, plex_server_name=None): """Retrieve a configured Plex server by name.""" + if DOMAIN not in hass.data: + raise HomeAssistantError("Plex integration not configured") plex_servers = hass.data[DOMAIN][SERVERS].values() + if not plex_servers: + raise HomeAssistantError("No Plex servers available") if plex_server_name: plex_server = next( @@ -79,25 +82,22 @@ def get_plex_server(hass, plex_server_name=None): ) if plex_server is not None: return plex_server - _LOGGER.error( - "Requested Plex server '%s' not found in %s", - plex_server_name, - [x.friendly_name for x in plex_servers], + friendly_names = [x.friendly_name for x in plex_servers] + raise HomeAssistantError( + f"Requested Plex server '{plex_server_name}' not found in {friendly_names}" ) - return None if len(plex_servers) == 1: return next(iter(plex_servers)) - _LOGGER.error( - "Multiple Plex servers configured, choose with 'plex_server' key: %s", - [x.friendly_name for x in plex_servers], + friendly_names = [x.friendly_name for x in plex_servers] + raise HomeAssistantError( + f"Multiple Plex servers configured, choose with 'plex_server' key: {friendly_names}" ) - return None def lookup_plex_media(hass, content_type, content_id): - """Look up Plex media using media_player.play_media service payloads.""" + """Look up Plex media for other integrations using media_player.play_media service payloads.""" content = json.loads(content_id) if isinstance(content, int): @@ -105,16 +105,37 @@ def lookup_plex_media(hass, content_type, content_id): content_type = DOMAIN plex_server_name = content.pop("plex_server", None) - shuffle = content.pop("shuffle", 0) + plex_server = get_plex_server(hass, plex_server_name) - plex_server = get_plex_server(hass, plex_server_name=plex_server_name) - if not plex_server: - return (None, None) + playqueue_id = content.pop("playqueue_id", None) + if playqueue_id: + try: + playqueue = plex_server.get_playqueue(playqueue_id) + except NotFound as err: + raise HomeAssistantError( + f"PlayQueue '{playqueue_id}' could not be found" + ) from err + else: + shuffle = content.pop("shuffle", 0) + media = plex_server.lookup_media(content_type, **content) + if media is None: + raise HomeAssistantError( + f"Plex media not found using payload: '{content_id}'" + ) + playqueue = plex_server.create_playqueue(media, shuffle=shuffle) - media = plex_server.lookup_media(content_type, **content) - if media is None: - _LOGGER.error("Media could not be found: %s", content) - return (None, None) - - playqueue = plex_server.create_playqueue(media, shuffle=shuffle) return (playqueue, plex_server) + + +def play_on_sonos(hass, content_type, content_id, speaker_name): + """Play music on a connected Sonos speaker using Plex APIs. + + Called by Sonos 'media_player.play_media' service. + """ + media, plex_server = lookup_plex_media(hass, content_type, content_id) + sonos_speaker = plex_server.account.sonos_speaker(speaker_name) + if sonos_speaker is None: + message = f"Sonos speaker '{speaker_name}' is not associated with '{plex_server.friendly_name}'" + _LOGGER.error(message) + raise HomeAssistantError(message) + sonos_speaker.playMedia(media) diff --git a/homeassistant/components/plex/translations/de.json b/homeassistant/components/plex/translations/de.json index 961ad4b3ed6..2ba14e65f85 100644 --- a/homeassistant/components/plex/translations/de.json +++ b/homeassistant/components/plex/translations/de.json @@ -3,9 +3,10 @@ "abort": { "all_configured": "Alle verkn\u00fcpften Server sind bereits konfiguriert", "already_configured": "Dieser Plex-Server ist bereits konfiguriert", - "already_in_progress": "Plex wird konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", "token_request_timeout": "Zeit\u00fcberschreitung beim Erhalt des Tokens", - "unknown": "Aus unbekanntem Grund fehlgeschlagen" + "unknown": "Unerwarteter Fehler" }, "error": { "faulty_credentials": "Autorisierung fehlgeschlagen, Token \u00fcberpr\u00fcfen", diff --git a/homeassistant/components/plex/translations/tr.json b/homeassistant/components/plex/translations/tr.json new file mode 100644 index 00000000000..93f8cc85eae --- /dev/null +++ b/homeassistant/components/plex/translations/tr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Bu Plex sunucusu zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "unknown": "Beklenmeyen hata" + }, + "step": { + "manual_setup": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + } + }, + "user": { + "title": "Plex Medya Sunucusu" + }, + "user_advanced": { + "data": { + "setup_method": "Kurulum y\u00f6ntemi" + }, + "title": "Plex Medya Sunucusu" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/translations/uk.json b/homeassistant/components/plex/translations/uk.json new file mode 100644 index 00000000000..20351cf735a --- /dev/null +++ b/homeassistant/components/plex/translations/uk.json @@ -0,0 +1,62 @@ +{ + "config": { + "abort": { + "all_configured": "\u0412\u0441\u0456 \u043f\u043e\u0432'\u044f\u0437\u0430\u043d\u0456 \u0441\u0435\u0440\u0432\u0435\u0440\u0438 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0456.", + "already_configured": "\u0426\u0435\u0439 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e", + "token_request_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0442\u043e\u043a\u0435\u043d\u0430.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "faulty_credentials": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457, \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0422\u043e\u043a\u0435\u043d.", + "host_or_token": "\u041d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u0432\u043a\u0430\u0437\u0430\u0442\u0438 \u0425\u043e\u0441\u0442 \u0430\u0431\u043e \u0422\u043e\u043a\u0435\u043d.", + "no_servers": "\u041d\u0435\u043c\u0430\u0454 \u0441\u0435\u0440\u0432\u0435\u0440\u0456\u0432, \u043f\u043e\u0432'\u044f\u0437\u0430\u043d\u0438\u0445 \u0437 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u043c \u0437\u0430\u043f\u0438\u0441\u043e\u043c.", + "not_found": "\u0421\u0435\u0440\u0432\u0435\u0440 Plex \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e.", + "ssl_error": "\u041f\u0440\u043e\u0431\u043b\u0435\u043c\u0430 \u0437 SSL \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u043c." + }, + "flow_title": "{name} ({host})", + "step": { + "manual_setup": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL", + "token": "\u0422\u043e\u043a\u0435\u043d (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)", + "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL" + }, + "title": "\u0420\u0443\u0447\u043d\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Plex" + }, + "select_server": { + "data": { + "server": "\u0421\u0435\u0440\u0432\u0435\u0440" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u0434\u0438\u043d \u0437 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0445 \u0441\u0435\u0440\u0432\u0435\u0440\u0456\u0432:", + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440 Plex" + }, + "user": { + "description": "\u041f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u043d\u0430 [plex.tv](https://plex.tv), \u0449\u043e\u0431 \u043f\u0440\u0438\u0432'\u044f\u0437\u0430\u0442\u0438 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0434\u043e Home Assistant.", + "title": "Plex Media Server" + }, + "user_advanced": { + "data": { + "setup_method": "\u0421\u043f\u043e\u0441\u0456\u0431 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f" + }, + "title": "Plex Media Server" + } + } + }, + "options": { + "step": { + "plex_mp_settings": { + "data": { + "ignore_new_shared_users": "\u0406\u0433\u043d\u043e\u0440\u0443\u0432\u0430\u0442\u0438 \u043d\u043e\u0432\u0438\u0445 \u043a\u0435\u0440\u043e\u0432\u0430\u043d\u0438\u0445 / \u0437\u0430\u0433\u0430\u043b\u044c\u043d\u0438\u0445 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0456\u0432", + "ignore_plex_web_clients": "\u0406\u0433\u043d\u043e\u0440\u0443\u0432\u0430\u0442\u0438 \u0432\u0435\u0431-\u043a\u043b\u0456\u0454\u043d\u0442\u0438 Plex", + "monitored_users": "\u0412\u0456\u0434\u0441\u0442\u0435\u0436\u0443\u0432\u0430\u043d\u0456 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0456", + "use_episode_art": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u043e\u0431\u043a\u043b\u0430\u0434\u0438\u043d\u043a\u0438 \u0435\u043f\u0456\u0437\u043e\u0434\u0456\u0432" + }, + "description": "\u0414\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0456 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plugwise/gateway.py b/homeassistant/components/plugwise/gateway.py index 3b61bd3930d..a0bf23986bd 100644 --- a/homeassistant/components/plugwise/gateway.py +++ b/homeassistant/components/plugwise/gateway.py @@ -12,7 +12,6 @@ from plugwise.exceptions import ( XMLDataMissingError, ) from plugwise.smile import Smile -import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -46,8 +45,6 @@ from .const import ( UNDO_UPDATE_LISTENER, ) -CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/plugwise/manifest.json b/homeassistant/components/plugwise/manifest.json index 5a32341139c..998b84fe5d4 100644 --- a/homeassistant/components/plugwise/manifest.json +++ b/homeassistant/components/plugwise/manifest.json @@ -2,7 +2,7 @@ "domain": "plugwise", "name": "Plugwise", "documentation": "https://www.home-assistant.io/integrations/plugwise", - "requirements": ["plugwise==0.8.3"], + "requirements": ["plugwise==0.8.5"], "codeowners": ["@CoMPaTech", "@bouwew", "@brefra"], "zeroconf": ["_plugwise._tcp.local."], "config_flow": true diff --git a/homeassistant/components/plugwise/sensor.py b/homeassistant/components/plugwise/sensor.py index 578f1bc7f7e..f57ff2b2a91 100644 --- a/homeassistant/components/plugwise/sensor.py +++ b/homeassistant/components/plugwise/sensor.py @@ -305,10 +305,7 @@ class PwThermostatSensor(SmileSensor, Entity): return if data.get(self._sensor) is not None: - measurement = data[self._sensor] - if self._unit_of_measurement == PERCENTAGE: - measurement = int(measurement * 100) - self._state = measurement + self._state = data[self._sensor] self._icon = CUSTOM_ICONS.get(self._sensor, self._icon) self.async_write_ha_state() @@ -380,10 +377,7 @@ class PwPowerSensor(SmileSensor, Entity): return if data.get(self._sensor) is not None: - measurement = data[self._sensor] - if self._unit_of_measurement == ENERGY_KILO_WATT_HOUR: - measurement = round((measurement / 1000), 1) - self._state = measurement + self._state = data[self._sensor] self._icon = CUSTOM_ICONS.get(self._sensor, self._icon) self.async_write_ha_state() diff --git a/homeassistant/components/plugwise/translations/de.json b/homeassistant/components/plugwise/translations/de.json index 2282e3584fc..685cd6fb9ae 100644 --- a/homeassistant/components/plugwise/translations/de.json +++ b/homeassistant/components/plugwise/translations/de.json @@ -4,7 +4,8 @@ "already_configured": "Service ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "flow_title": "Smile: {name}", @@ -17,6 +18,8 @@ }, "user_gateway": { "data": { + "host": "IP-Adresse", + "password": "Smile ID", "port": "Port" }, "description": "Bitte eingeben" diff --git a/homeassistant/components/plugwise/translations/lb.json b/homeassistant/components/plugwise/translations/lb.json index 4ce9f8b0145..a3618bc911e 100644 --- a/homeassistant/components/plugwise/translations/lb.json +++ b/homeassistant/components/plugwise/translations/lb.json @@ -21,7 +21,8 @@ "data": { "host": "IP Adress", "password": "Smile ID", - "port": "Port" + "port": "Port", + "username": "Smile Benotzernumm" }, "title": "Mam Smile verbannen" } diff --git a/homeassistant/components/plugwise/translations/tr.json b/homeassistant/components/plugwise/translations/tr.json index d25f1975cf7..60d6b1f92be 100644 --- a/homeassistant/components/plugwise/translations/tr.json +++ b/homeassistant/components/plugwise/translations/tr.json @@ -1,10 +1,23 @@ { "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "flow_title": "Smile: {name}", "step": { "user_gateway": { "data": { + "host": "\u0130p Adresi", + "password": "G\u00fcl\u00fcmseme Kimli\u011fi", + "port": "Port", "username": "Smile Kullan\u0131c\u0131 Ad\u0131" - } + }, + "description": "L\u00fctfen girin" } } } diff --git a/homeassistant/components/plugwise/translations/uk.json b/homeassistant/components/plugwise/translations/uk.json new file mode 100644 index 00000000000..6c6f54612b1 --- /dev/null +++ b/homeassistant/components/plugwise/translations/uk.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "flow_title": "Smile: {name}", + "step": { + "user": { + "data": { + "flow_type": "\u0422\u0438\u043f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f" + }, + "description": "\u041f\u0440\u043e\u0434\u0443\u043a\u0442:", + "title": "\u0422\u0438\u043f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Plugwise" + }, + "user_gateway": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "password": "Smile ID", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u041b\u043e\u0433\u0456\u043d Smile" + }, + "description": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0432\u0432\u0435\u0434\u0456\u0442\u044c:", + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e Smile" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u0441\u043a\u0430\u043d\u0443\u0432\u0430\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Plugwise" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/de.json b/homeassistant/components/plum_lightpad/translations/de.json index accee16a6f5..c94bf9aadab 100644 --- a/homeassistant/components/plum_lightpad/translations/de.json +++ b/homeassistant/components/plum_lightpad/translations/de.json @@ -1,7 +1,10 @@ { "config": { + "abort": { + "already_configured": "Konto wurde bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "user": { diff --git a/homeassistant/components/plum_lightpad/translations/tr.json b/homeassistant/components/plum_lightpad/translations/tr.json new file mode 100644 index 00000000000..f0dab20775f --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "E-posta" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/plum_lightpad/translations/uk.json b/homeassistant/components/plum_lightpad/translations/uk.json new file mode 100644 index 00000000000..96b14f79375 --- /dev/null +++ b/homeassistant/components/plum_lightpad/translations/uk.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/point/translations/de.json b/homeassistant/components/point/translations/de.json index 8ee83eab727..343c02055d5 100644 --- a/homeassistant/components/point/translations/de.json +++ b/homeassistant/components/point/translations/de.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_setup": "Du kannst nur ein Point-Konto konfigurieren.", + "already_setup": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", "authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL.", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", "external_setup": "Pointt erfolgreich von einem anderen Flow konfiguriert.", - "no_flows": "Du m\u00fcsst Point konfigurieren, bevor du dich damit authentifizieren kannst. [Bitte lese die Anweisungen] (https://www.home-assistant.io/components/point/).", + "no_flows": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", "unknown_authorize_url_generation": "Beim Generieren einer Authentifizierungs-URL ist ein unbekannter Fehler aufgetreten" }, "create_entry": { @@ -17,15 +17,15 @@ }, "step": { "auth": { - "description": "Folge dem Link unten und Akzeptiere Zugriff auf dei Minut-Konto. Kehre dann zur\u00fcck und dr\u00fccke unten auf Senden . \n\n [Link]({authorization_url})", + "description": "Folge dem Link unten und **Best\u00e4tige** den Zugriff auf dein Minut-Konto. Kehre dann zur\u00fcck und dr\u00fccke unten auf **Senden**. \n\n [Link]({authorization_url})", "title": "Point authentifizieren" }, "user": { "data": { "flow_impl": "Anbieter" }, - "description": "W\u00e4hle \u00fcber welchen Authentifizierungsanbieter du sich mit Point authentifizieren m\u00f6chtest.", - "title": "Authentifizierungsanbieter" + "description": "M\u00f6chtest du mit der Einrichtung beginnen?", + "title": "W\u00e4hle die Authentifizierungsmethode" } } } diff --git a/homeassistant/components/point/translations/fr.json b/homeassistant/components/point/translations/fr.json index 141af3545ba..ab9cd7af34e 100644 --- a/homeassistant/components/point/translations/fr.json +++ b/homeassistant/components/point/translations/fr.json @@ -5,7 +5,8 @@ "authorize_url_fail": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation.", "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", "external_setup": "Point correctement configur\u00e9 \u00e0 partir d\u2019un autre flux.", - "no_flows": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation." + "no_flows": "Le composant n'est pas configur\u00e9. Veuillez suivre la documentation.", + "unknown_authorize_url_generation": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation." }, "create_entry": { "default": "Authentification r\u00e9ussie" diff --git a/homeassistant/components/point/translations/tr.json b/homeassistant/components/point/translations/tr.json new file mode 100644 index 00000000000..5a4849fad07 --- /dev/null +++ b/homeassistant/components/point/translations/tr.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_setup": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "unknown_authorize_url_generation": "Yetkilendirme url'si olu\u015fturulurken bilinmeyen hata." + }, + "error": { + "no_token": "Eri\u015fim Belirteci" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/point/translations/uk.json b/homeassistant/components/point/translations/uk.json new file mode 100644 index 00000000000..6b66a39a291 --- /dev/null +++ b/homeassistant/components/point/translations/uk.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_setup": "\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.", + "authorize_url_fail": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "external_setup": "Point \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439 \u0437 \u0456\u043d\u0448\u043e\u0433\u043e \u043f\u043e\u0442\u043e\u043a\u0443.", + "no_flows": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", + "unknown_authorize_url_generation": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457." + }, + "create_entry": { + "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." + }, + "error": { + "follow_link": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0437\u0430 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f\u043c \u0456 \u043f\u0440\u043e\u0439\u0434\u0456\u0442\u044c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e, \u043f\u0435\u0440\u0448 \u043d\u0456\u0436 \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0438 \"\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0438\".", + "no_token": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443." + }, + "step": { + "auth": { + "description": "\u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0437\u0430 [\u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f\u043c]({authorization_url}) \u0456 ** \u0414\u043e\u0437\u0432\u043e\u043b\u044c\u0442\u0435 ** \u0434\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u0432\u0430\u0448\u043e\u0433\u043e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 Minut, \u043f\u043e\u0442\u0456\u043c \u043f\u043e\u0432\u0435\u0440\u043d\u0456\u0442\u044c\u0441\u044f \u0441\u044e\u0434\u0438 \u0442\u0430 \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c ** \u041f\u0406\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u0418 **.", + "title": "Minut Point" + }, + "user": { + "data": { + "flow_impl": "\u041f\u0440\u043e\u0432\u0430\u0439\u0434\u0435\u0440" + }, + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?", + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/poolsense/translations/de.json b/homeassistant/components/poolsense/translations/de.json index c7dfe6d02b2..5869da61c9c 100644 --- a/homeassistant/components/poolsense/translations/de.json +++ b/homeassistant/components/poolsense/translations/de.json @@ -3,13 +3,16 @@ "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, "step": { "user": { "data": { "email": "E-Mail", "password": "Passwort" }, - "description": "Wollen Sie mit der Einrichtung beginnen?" + "description": "M\u00f6chtest du mit der Einrichtung beginnen?" } } } diff --git a/homeassistant/components/poolsense/translations/tr.json b/homeassistant/components/poolsense/translations/tr.json new file mode 100644 index 00000000000..1e2e9d0c5b8 --- /dev/null +++ b/homeassistant/components/poolsense/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "email": "E-posta", + "password": "Parola" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/poolsense/translations/uk.json b/homeassistant/components/poolsense/translations/uk.json new file mode 100644 index 00000000000..6ac3b97f741 --- /dev/null +++ b/homeassistant/components/poolsense/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "step": { + "user": { + "data": { + "email": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?", + "title": "PoolSense" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index fcf44fcf9b1..54b7310b7ad 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -5,9 +5,8 @@ import logging import requests from tesla_powerwall import MissingAttributeError, Powerwall, PowerwallUnreachableError -import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_IP_ADDRESS from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady @@ -32,10 +31,7 @@ from .const import ( UPDATE_INTERVAL, ) -CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.Schema({vol.Required(CONF_IP_ADDRESS): cv.string})}, - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = cv.deprecated(DOMAIN) PLATFORMS = ["binary_sensor", "sensor"] @@ -45,18 +41,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup(hass: HomeAssistant, config: dict): """Set up the Tesla Powerwall component.""" hass.data.setdefault(DOMAIN, {}) - conf = config.get(DOMAIN) - if not conf: - return True - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=conf, - ) - ) return True diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py index eb23ce4e030..37ee2730bb4 100644 --- a/homeassistant/components/powerwall/config_flow.py +++ b/homeassistant/components/powerwall/config_flow.py @@ -5,19 +5,19 @@ from tesla_powerwall import MissingAttributeError, Powerwall, PowerwallUnreachab import voluptuous as vol from homeassistant import config_entries, core, exceptions +from homeassistant.components.dhcp import IP_ADDRESS from homeassistant.const import CONF_IP_ADDRESS +from homeassistant.core import callback from .const import DOMAIN # pylint:disable=unused-import _LOGGER = logging.getLogger(__name__) -DATA_SCHEMA = vol.Schema({vol.Required(CONF_IP_ADDRESS): str}) - async def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect. - Data has the keys from DATA_SCHEMA with values provided by the user. + Data has the keys from schema with values provided by the user. """ power_wall = Powerwall(data[CONF_IP_ADDRESS]) @@ -42,6 +42,20 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + def __init__(self): + """Initialize the powerwall flow.""" + self.ip_address = None + + async def async_step_dhcp(self, dhcp_discovery): + """Handle dhcp discovery.""" + if self._async_ip_address_already_configured(dhcp_discovery[IP_ADDRESS]): + return self.async_abort(reason="already_configured") + + self.ip_address = dhcp_discovery[IP_ADDRESS] + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context["title_placeholders"] = {CONF_IP_ADDRESS: self.ip_address} + return await self.async_step_user() + async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} @@ -62,15 +76,20 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=info["title"], data=user_input) return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors + step_id="user", + data_schema=vol.Schema( + {vol.Required(CONF_IP_ADDRESS, default=self.ip_address): str} + ), + errors=errors, ) - async def async_step_import(self, user_input): - """Handle import.""" - await self.async_set_unique_id(user_input[CONF_IP_ADDRESS]) - self._abort_if_unique_id_configured() - - return await self.async_step_user(user_input) + @callback + def _async_ip_address_already_configured(self, ip_address): + """See if we already have an entry matching the ip_address.""" + for entry in self._async_current_entries(): + if entry.data.get(CONF_IP_ADDRESS) == ip_address: + return True + return False class CannotConnect(exceptions.HomeAssistantError): diff --git a/homeassistant/components/powerwall/manifest.json b/homeassistant/components/powerwall/manifest.json index be83f6825e7..6b7b147d3c5 100644 --- a/homeassistant/components/powerwall/manifest.json +++ b/homeassistant/components/powerwall/manifest.json @@ -4,5 +4,9 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/powerwall", "requirements": ["tesla-powerwall==0.3.3"], - "codeowners": ["@bdraco", "@jrester"] + "codeowners": ["@bdraco", "@jrester"], + "dhcp": [ + {"hostname":"1118431-*","macaddress":"88DA1A*"}, + {"hostname":"1118431-*","macaddress":"000145*"} + ] } diff --git a/homeassistant/components/powerwall/strings.json b/homeassistant/components/powerwall/strings.json index 86b7db3e909..ac0d9568154 100644 --- a/homeassistant/components/powerwall/strings.json +++ b/homeassistant/components/powerwall/strings.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "Tesla Powerwall ({ip_address})", "step": { "user": { "title": "Connect to the powerwall", diff --git a/homeassistant/components/powerwall/translations/ca.json b/homeassistant/components/powerwall/translations/ca.json index 2c9becb1795..4b176fff686 100644 --- a/homeassistant/components/powerwall/translations/ca.json +++ b/homeassistant/components/powerwall/translations/ca.json @@ -8,6 +8,7 @@ "unknown": "Error inesperat", "wrong_version": "El teu Powerwall utilitza una versi\u00f3 de programari no compatible. L'hauries d'actualitzar o informar d'aquest problema perqu\u00e8 sigui solucionat." }, + "flow_title": "Tesla Powerwall ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/cs.json b/homeassistant/components/powerwall/translations/cs.json index b64eabcf33b..698934ad10e 100644 --- a/homeassistant/components/powerwall/translations/cs.json +++ b/homeassistant/components/powerwall/translations/cs.json @@ -8,6 +8,7 @@ "unknown": "Neo\u010dek\u00e1van\u00e1 chyba", "wrong_version": "Powerwall pou\u017e\u00edv\u00e1 verzi softwaru, kter\u00e1 nen\u00ed podporov\u00e1na. Zva\u017ete upgrade nebo nahlaste probl\u00e9m, aby mohl b\u00fdt vy\u0159e\u0161en." }, + "flow_title": "Tesla Powerwall ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/de.json b/homeassistant/components/powerwall/translations/de.json index f5317e3046a..c30286d8744 100644 --- a/homeassistant/components/powerwall/translations/de.json +++ b/homeassistant/components/powerwall/translations/de.json @@ -1,12 +1,13 @@ { "config": { "abort": { - "already_configured": "Die Powerwall ist bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler" }, + "flow_title": "Tesla Powerwall ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/en.json b/homeassistant/components/powerwall/translations/en.json index 773daace5c0..6eb0b77708d 100644 --- a/homeassistant/components/powerwall/translations/en.json +++ b/homeassistant/components/powerwall/translations/en.json @@ -8,6 +8,7 @@ "unknown": "Unexpected error", "wrong_version": "Your powerwall uses a software version that is not supported. Please consider upgrading or reporting this issue so it can be resolved." }, + "flow_title": "Tesla Powerwall ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/es.json b/homeassistant/components/powerwall/translations/es.json index 76835e81480..373bf29f8ba 100644 --- a/homeassistant/components/powerwall/translations/es.json +++ b/homeassistant/components/powerwall/translations/es.json @@ -8,6 +8,7 @@ "unknown": "Error inesperado", "wrong_version": "Tu powerwall utiliza una versi\u00f3n de software que no es compatible. Considera actualizar o informar de este problema para que pueda resolverse." }, + "flow_title": "Powerwall de Tesla ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/et.json b/homeassistant/components/powerwall/translations/et.json index 996b0ea4b30..b10dca9b08b 100644 --- a/homeassistant/components/powerwall/translations/et.json +++ b/homeassistant/components/powerwall/translations/et.json @@ -8,6 +8,7 @@ "unknown": "Ootamatu t\u00f5rge", "wrong_version": "Teie Powerwall kasutab tarkvaraversiooni, mida ei toetata. Kaaluge tarkvara uuendamist v\u00f5i probleemist teavitamist, et see saaks lahendatud." }, + "flow_title": "Tesla Powerwall ( {ip_address} )", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/it.json b/homeassistant/components/powerwall/translations/it.json index 422a28b6936..376168f8616 100644 --- a/homeassistant/components/powerwall/translations/it.json +++ b/homeassistant/components/powerwall/translations/it.json @@ -8,6 +8,7 @@ "unknown": "Errore imprevisto", "wrong_version": "Il tuo powerwall utilizza una versione del software non supportata. Si prega di considerare l'aggiornamento o la segnalazione di questo problema in modo che possa essere risolto." }, + "flow_title": "Tesla Powerwall ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/no.json b/homeassistant/components/powerwall/translations/no.json index 7ddab100358..cdc04a006ad 100644 --- a/homeassistant/components/powerwall/translations/no.json +++ b/homeassistant/components/powerwall/translations/no.json @@ -8,6 +8,7 @@ "unknown": "Uventet feil", "wrong_version": "Powerwall bruker en programvareversjon som ikke st\u00f8ttes. Vennligst vurder \u00e5 oppgradere eller rapportere dette problemet, s\u00e5 det kan l\u00f8ses." }, + "flow_title": "", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/pl.json b/homeassistant/components/powerwall/translations/pl.json index 8532066608c..dfd4fa21a37 100644 --- a/homeassistant/components/powerwall/translations/pl.json +++ b/homeassistant/components/powerwall/translations/pl.json @@ -8,6 +8,7 @@ "unknown": "Nieoczekiwany b\u0142\u0105d", "wrong_version": "Powerwall u\u017cywa wersji oprogramowania, kt\u00f3ra nie jest obs\u0142ugiwana. Rozwa\u017c uaktualnienie lub zg\u0142oszenie tego problemu, aby mo\u017cna go by\u0142o rozwi\u0105za\u0107." }, + "flow_title": "Tesla UPS ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/ru.json b/homeassistant/components/powerwall/translations/ru.json index a8713bcd04a..faabf2d0ede 100644 --- a/homeassistant/components/powerwall/translations/ru.json +++ b/homeassistant/components/powerwall/translations/ru.json @@ -8,6 +8,7 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430.", "wrong_version": "\u0412\u0430\u0448 powerwall \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0432\u0435\u0440\u0441\u0438\u044e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043c\u043d\u043e\u0433\u043e \u043e\u0431\u0435\u0441\u043f\u0435\u0447\u0435\u043d\u0438\u044f, \u043a\u043e\u0442\u043e\u0440\u0430\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0440\u0430\u0441\u0441\u043c\u043e\u0442\u0440\u0438\u0442\u0435 \u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442\u044c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0438\u043b\u0438 \u0441\u043e\u043e\u0431\u0449\u0438\u0442\u0435 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0435, \u0447\u0442\u043e\u0431\u044b \u0435\u0435 \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u0440\u0435\u0448\u0438\u0442\u044c." }, + "flow_title": "Tesla Powerwall ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/tr.json b/homeassistant/components/powerwall/translations/tr.json new file mode 100644 index 00000000000..dd09a83a78c --- /dev/null +++ b/homeassistant/components/powerwall/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "flow_title": "Tesla Powerwall ( {ip_address} )", + "step": { + "user": { + "data": { + "ip_address": "\u0130p Adresi" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/translations/uk.json b/homeassistant/components/powerwall/translations/uk.json new file mode 100644 index 00000000000..9b397138c52 --- /dev/null +++ b/homeassistant/components/powerwall/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430", + "wrong_version": "\u0412\u0430\u0448 Powerwall \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454 \u0432\u0435\u0440\u0441\u0456\u044e \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u043d\u043e\u0433\u043e \u0437\u0430\u0431\u0435\u0437\u043f\u0435\u0447\u0435\u043d\u043d\u044f, \u044f\u043a\u0430 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0440\u043e\u0437\u0433\u043b\u044f\u043d\u044c\u0442\u0435 \u043c\u043e\u0436\u043b\u0438\u0432\u0456\u0441\u0442\u044c \u043f\u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0430\u0431\u043e \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u0442\u0435 \u043f\u0440\u043e \u0446\u044e \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0443, \u0449\u043e\u0431 \u0457\u0457 \u043c\u043e\u0436\u043d\u0430 \u0431\u0443\u043b\u043e \u0432\u0438\u0440\u0456\u0448\u0438\u0442\u0438." + }, + "step": { + "user": { + "data": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430" + }, + "title": "Tesla Powerwall" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/translations/zh-Hant.json b/homeassistant/components/powerwall/translations/zh-Hant.json index 45edbf2d88e..ec0d2e278b6 100644 --- a/homeassistant/components/powerwall/translations/zh-Hant.json +++ b/homeassistant/components/powerwall/translations/zh-Hant.json @@ -8,6 +8,7 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4", "wrong_version": "\u4e0d\u652f\u63f4\u60a8\u6240\u4f7f\u7528\u7684 Powerwall \u7248\u672c\u3002\u8acb\u8003\u616e\u9032\u884c\u5347\u7d1a\u6216\u56de\u5831\u6b64\u554f\u984c\u3001\u4ee5\u671f\u554f\u984c\u53ef\u4ee5\u7372\u5f97\u89e3\u6c7a\u3002" }, + "flow_title": "Tesla Powerwall ({ip_address})", "step": { "user": { "data": { diff --git a/homeassistant/components/profiler/translations/de.json b/homeassistant/components/profiler/translations/de.json new file mode 100644 index 00000000000..7137cd2ee4e --- /dev/null +++ b/homeassistant/components/profiler/translations/de.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "step": { + "user": { + "description": "M\u00f6chtest du mit der Einrichtung beginnen?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/profiler/translations/tr.json b/homeassistant/components/profiler/translations/tr.json new file mode 100644 index 00000000000..a152eb19468 --- /dev/null +++ b/homeassistant/components/profiler/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/profiler/translations/uk.json b/homeassistant/components/profiler/translations/uk.json new file mode 100644 index 00000000000..5594895456e --- /dev/null +++ b/homeassistant/components/profiler/translations/uk.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "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." + }, + "step": { + "user": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/de.json b/homeassistant/components/progettihwsw/translations/de.json index 2e5bed4b668..0f773e03c1d 100644 --- a/homeassistant/components/progettihwsw/translations/de.json +++ b/homeassistant/components/progettihwsw/translations/de.json @@ -1,7 +1,10 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/progettihwsw/translations/tr.json b/homeassistant/components/progettihwsw/translations/tr.json new file mode 100644 index 00000000000..1d3d77584dd --- /dev/null +++ b/homeassistant/components/progettihwsw/translations/tr.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "relay_modes": { + "data": { + "relay_1": "R\u00f6le 1", + "relay_10": "R\u00f6le 10", + "relay_11": "R\u00f6le 11", + "relay_12": "R\u00f6le 12", + "relay_13": "R\u00f6le 13", + "relay_14": "R\u00f6le 14", + "relay_15": "R\u00f6le 15", + "relay_16": "R\u00f6le 16", + "relay_2": "R\u00f6le 2", + "relay_3": "R\u00f6le 3", + "relay_4": "R\u00f6le 4", + "relay_5": "R\u00f6le 5", + "relay_6": "R\u00f6le 6", + "relay_7": "R\u00f6le 7", + "relay_8": "R\u00f6le 8", + "relay_9": "R\u00f6le 9" + }, + "title": "R\u00f6leleri kur" + }, + "user": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + }, + "title": "Panoyu kur" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/progettihwsw/translations/uk.json b/homeassistant/components/progettihwsw/translations/uk.json new file mode 100644 index 00000000000..7918db8e158 --- /dev/null +++ b/homeassistant/components/progettihwsw/translations/uk.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "relay_modes": { + "data": { + "relay_1": "\u0420\u0435\u043b\u0435 1", + "relay_10": "\u0420\u0435\u043b\u0435 10", + "relay_11": "\u0420\u0435\u043b\u0435 11", + "relay_12": "\u0420\u0435\u043b\u0435 12", + "relay_13": "\u0420\u0435\u043b\u0435 13", + "relay_14": "\u0420\u0435\u043b\u0435 14", + "relay_15": "\u0420\u0435\u043b\u0435 15", + "relay_16": "\u0420\u0435\u043b\u0435 16", + "relay_2": "\u0420\u0435\u043b\u0435 2", + "relay_3": "\u0420\u0435\u043b\u0435 3", + "relay_4": "\u0420\u0435\u043b\u0435 4", + "relay_5": "\u0420\u0435\u043b\u0435 5", + "relay_6": "\u0420\u0435\u043b\u0435 6", + "relay_7": "\u0420\u0435\u043b\u0435 7", + "relay_8": "\u0420\u0435\u043b\u0435 8", + "relay_9": "\u0420\u0435\u043b\u0435 9" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0440\u0435\u043b\u0435" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u043b\u0430\u0442\u0438" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/proxmoxve/__init__.py b/homeassistant/components/proxmoxve/__init__.py index 0919feb15e3..2f42ca8fe9e 100644 --- a/homeassistant/components/proxmoxve/__init__.py +++ b/homeassistant/components/proxmoxve/__init__.py @@ -1,9 +1,10 @@ """Support for Proxmox VE.""" -from enum import Enum +from datetime import timedelta import logging from proxmoxer import ProxmoxAPI from proxmoxer.backends.https import AuthenticationError +from proxmoxer.core import ResourceException from requests.exceptions import SSLError import voluptuous as vol @@ -14,11 +15,14 @@ from homeassistant.const import ( CONF_USERNAME, CONF_VERIFY_SSL, ) +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.update_coordinator import ( + CoordinatorEntity, + DataUpdateCoordinator, +) -_LOGGER = logging.getLogger(__name__) - - +PLATFORMS = ["binary_sensor"] DOMAIN = "proxmoxve" PROXMOX_CLIENTS = "proxmox_clients" CONF_REALM = "realm" @@ -27,9 +31,17 @@ CONF_NODES = "nodes" CONF_VMS = "vms" CONF_CONTAINERS = "containers" +COORDINATOR = "coordinator" +API_DATA = "api_data" + DEFAULT_PORT = 8006 DEFAULT_REALM = "pam" DEFAULT_VERIFY_SSL = True +TYPE_VM = 0 +TYPE_CONTAINER = 1 +UPDATE_INTERVAL = 60 + +_LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( { @@ -71,52 +83,191 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass, config): - """Set up the component.""" +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the platform.""" + hass.data.setdefault(DOMAIN, {}) - # Create API Clients for later use - hass.data[PROXMOX_CLIENTS] = {} - for entry in config[DOMAIN]: - host = entry[CONF_HOST] - port = entry[CONF_PORT] - user = entry[CONF_USERNAME] - realm = entry[CONF_REALM] - password = entry[CONF_PASSWORD] - verify_ssl = entry[CONF_VERIFY_SSL] + def build_client() -> ProxmoxAPI: + """Build the Proxmox client connection.""" + hass.data[PROXMOX_CLIENTS] = {} + for entry in config[DOMAIN]: + host = entry[CONF_HOST] + port = entry[CONF_PORT] + user = entry[CONF_USERNAME] + realm = entry[CONF_REALM] + password = entry[CONF_PASSWORD] + verify_ssl = entry[CONF_VERIFY_SSL] - try: - # Construct an API client with the given data for the given host - proxmox_client = ProxmoxClient( - host, port, user, realm, password, verify_ssl + try: + # Construct an API client with the given data for the given host + proxmox_client = ProxmoxClient( + host, port, user, realm, password, verify_ssl + ) + proxmox_client.build_client() + except AuthenticationError: + _LOGGER.warning( + "Invalid credentials for proxmox instance %s:%d", host, port + ) + continue + except SSLError: + _LOGGER.error( + 'Unable to verify proxmox server SSL. Try using "verify_ssl: false"' + ) + continue + + return proxmox_client + + proxmox_client = await hass.async_add_executor_job(build_client) + + async def async_update_data() -> dict: + """Fetch data from API endpoint.""" + + proxmox = proxmox_client.get_api_client() + + def poll_api() -> dict: + data = {} + + for host_config in config[DOMAIN]: + host_name = host_config["host"] + + data[host_name] = {} + + for node_config in host_config["nodes"]: + node_name = node_config["node"] + data[host_name][node_name] = {} + + for vm_id in node_config["vms"]: + data[host_name][node_name][vm_id] = {} + + vm_status = call_api_container_vm( + proxmox, node_name, vm_id, TYPE_VM + ) + + if vm_status is None: + _LOGGER.warning("Vm/Container %s unable to be found", vm_id) + data[host_name][node_name][vm_id] = None + continue + + data[host_name][node_name][vm_id] = parse_api_container_vm( + vm_status + ) + + for container_id in node_config["containers"]: + data[host_name][node_name][container_id] = {} + + container_status = call_api_container_vm( + proxmox, node_name, container_id, TYPE_CONTAINER + ) + + if container_status is None: + _LOGGER.error( + "Vm/Container %s unable to be found", container_id + ) + data[host_name][node_name][container_id] = None + continue + + data[host_name][node_name][ + container_id + ] = parse_api_container_vm(container_status) + + return data + + return await hass.async_add_executor_job(poll_api) + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="proxmox_coordinator", + update_method=async_update_data, + update_interval=timedelta(seconds=UPDATE_INTERVAL), + ) + + hass.data[DOMAIN][COORDINATOR] = coordinator + + # Fetch initial data + await coordinator.async_refresh() + + for component in PLATFORMS: + await hass.async_create_task( + hass.helpers.discovery.async_load_platform( + component, DOMAIN, {"config": config}, config ) - proxmox_client.build_client() - except AuthenticationError: - _LOGGER.warning( - "Invalid credentials for proxmox instance %s:%d", host, port - ) - continue - except SSLError: - _LOGGER.error( - 'Unable to verify proxmox server SSL. Try using "verify_ssl: false"' - ) - continue - - hass.data[PROXMOX_CLIENTS][f"{host}:{port}"] = proxmox_client - - if hass.data[PROXMOX_CLIENTS]: - hass.helpers.discovery.load_platform( - "binary_sensor", DOMAIN, {"entries": config[DOMAIN]}, config ) - return True - return False + return True -class ProxmoxItemType(Enum): - """Represents the different types of machines in Proxmox.""" +def parse_api_container_vm(status): + """Get the container or vm api data and return it formatted in a dictionary. - qemu = 0 - lxc = 1 + It is implemented in this way to allow for more data to be added for sensors + in the future. + """ + + return {"status": status["status"], "name": status["name"]} + + +def call_api_container_vm(proxmox, node_name, vm_id, machine_type): + """Make proper api calls.""" + status = None + + try: + if machine_type == TYPE_VM: + status = proxmox.nodes(node_name).qemu(vm_id).status.current.get() + elif machine_type == TYPE_CONTAINER: + status = proxmox.nodes(node_name).lxc(vm_id).status.current.get() + except ResourceException: + return None + + return status + + +class ProxmoxEntity(CoordinatorEntity): + """Represents any entity created for the Proxmox VE platform.""" + + def __init__( + self, + coordinator: DataUpdateCoordinator, + unique_id, + name, + icon, + host_name, + node_name, + vm_id=None, + ): + """Initialize the Proxmox entity.""" + super().__init__(coordinator) + + self.coordinator = coordinator + self._unique_id = unique_id + self._name = name + self._host_name = host_name + self._icon = icon + self._available = True + self._node_name = node_name + self._vm_id = vm_id + + self._state = None + + @property + def unique_id(self) -> str: + """Return the unique ID for this sensor.""" + return self._unique_id + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name + + @property + def icon(self) -> str: + """Return the mdi icon of the entity.""" + return self._icon + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self.coordinator.last_update_success and self._available class ProxmoxClient: diff --git a/homeassistant/components/proxmoxve/binary_sensor.py b/homeassistant/components/proxmoxve/binary_sensor.py index 698a2c35ae1..014766b532e 100644 --- a/homeassistant/components/proxmoxve/binary_sensor.py +++ b/homeassistant/components/proxmoxve/binary_sensor.py @@ -1,112 +1,95 @@ """Binary sensor to read Proxmox VE data.""" import logging -from homeassistant.components.binary_sensor import BinarySensorEntity -from homeassistant.const import ATTR_ATTRIBUTION, CONF_HOST, CONF_PORT +from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from . import CONF_CONTAINERS, CONF_NODES, CONF_VMS, PROXMOX_CLIENTS, ProxmoxItemType +from . import COORDINATOR, DOMAIN, ProxmoxEntity -ATTRIBUTION = "Data provided by Proxmox VE" _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the sensor platform.""" +async def async_setup_platform(hass, config, add_entities, discovery_info=None): + """Set up binary sensors.""" + if discovery_info is None: + return + + coordinator = hass.data[DOMAIN][COORDINATOR] sensors = [] - for entry in discovery_info["entries"]: - port = entry[CONF_PORT] + for host_config in discovery_info["config"][DOMAIN]: + host_name = host_config["host"] - for node in entry[CONF_NODES]: - for virtual_machine in node[CONF_VMS]: - sensors.append( - ProxmoxBinarySensor( - hass.data[PROXMOX_CLIENTS][f"{entry[CONF_HOST]}:{port}"], - node["node"], - ProxmoxItemType.qemu, - virtual_machine, - ) + for node_config in host_config["nodes"]: + node_name = node_config["node"] + + for vm_id in node_config["vms"]: + coordinator_data = coordinator.data[host_name][node_name][vm_id] + + # unfound vm case + if coordinator_data is None: + continue + + vm_name = coordinator_data["name"] + vm_status = create_binary_sensor( + coordinator, host_name, node_name, vm_id, vm_name ) + sensors.append(vm_status) - for container in node[CONF_CONTAINERS]: - sensors.append( - ProxmoxBinarySensor( - hass.data[PROXMOX_CLIENTS][f"{entry[CONF_HOST]}:{port}"], - node["node"], - ProxmoxItemType.lxc, - container, - ) + for container_id in node_config["containers"]: + coordinator_data = coordinator.data[host_name][node_name][container_id] + + # unfound container case + if coordinator_data is None: + continue + + container_name = coordinator_data["name"] + container_status = create_binary_sensor( + coordinator, host_name, node_name, container_id, container_name ) + sensors.append(container_status) - add_entities(sensors, True) + add_entities(sensors) -class ProxmoxBinarySensor(BinarySensorEntity): +def create_binary_sensor(coordinator, host_name, node_name, vm_id, name): + """Create a binary sensor based on the given data.""" + return ProxmoxBinarySensor( + coordinator=coordinator, + unique_id=f"proxmox_{node_name}_{vm_id}_running", + name=f"{node_name}_{name}_running", + icon="", + host_name=host_name, + node_name=node_name, + vm_id=vm_id, + ) + + +class ProxmoxBinarySensor(ProxmoxEntity): """A binary sensor for reading Proxmox VE data.""" - def __init__(self, proxmox_client, item_node, item_type, item_id): - """Initialize the binary sensor.""" - self._proxmox_client = proxmox_client - self._item_node = item_node - self._item_type = item_type - self._item_id = item_id - - self._vmname = None - self._name = None + def __init__( + self, + coordinator: DataUpdateCoordinator, + unique_id, + name, + icon, + host_name, + node_name, + vm_id, + ): + """Create the binary sensor for vms or containers.""" + super().__init__( + coordinator, unique_id, name, icon, host_name, node_name, vm_id + ) self._state = None @property - def name(self): - """Return the name of the entity.""" - return self._name - - @property - def is_on(self): - """Return true if VM/container is running.""" - return self._state - - @property - def device_state_attributes(self): - """Return device attributes of the entity.""" - return { - "node": self._item_node, - "vmid": self._item_id, - "vmname": self._vmname, - "type": self._item_type.name, - ATTR_ATTRIBUTION: ATTRIBUTION, - } - - def update(self): - """Check if the VM/Container is running.""" - item = self.poll_item() - - if item is None: - _LOGGER.warning("Failed to poll VM/container %s", self._item_id) - return - - self._state = item["status"] == "running" - - def poll_item(self): - """Find the VM/Container with the set item_id.""" - items = ( - self._proxmox_client.get_api_client() - .nodes(self._item_node) - .get(self._item_type.name) - ) - item = next( - (item for item in items if item["vmid"] == str(self._item_id)), None - ) - - if item is None: - _LOGGER.warning("Couldn't find VM/Container with the ID %s", self._item_id) - return None - - if self._vmname is None: - self._vmname = item["name"] - - if self._name is None: - self._name = f"{self._item_node} {self._vmname} running" - - return item + def state(self): + """Return the state of the binary sensor.""" + data = self.coordinator.data[self._host_name][self._node_name][self._vm_id] + if data["status"] == "running": + return STATE_ON + return STATE_OFF diff --git a/homeassistant/components/proxy/manifest.json b/homeassistant/components/proxy/manifest.json index 081645a4aa8..65d8d21fc0c 100644 --- a/homeassistant/components/proxy/manifest.json +++ b/homeassistant/components/proxy/manifest.json @@ -2,6 +2,6 @@ "domain": "proxy", "name": "Camera Proxy", "documentation": "https://www.home-assistant.io/integrations/proxy", - "requirements": ["pillow==7.2.0"], + "requirements": ["pillow==8.1.0"], "codeowners": [] } diff --git a/homeassistant/components/ps4/translations/de.json b/homeassistant/components/ps4/translations/de.json index 5dd638a717c..d5aa867f1db 100644 --- a/homeassistant/components/ps4/translations/de.json +++ b/homeassistant/components/ps4/translations/de.json @@ -1,15 +1,16 @@ { "config": { "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", "credential_error": "Fehler beim Abrufen der Anmeldeinformationen.", "no_devices_found": "Es wurden keine PlayStation 4 im Netzwerk gefunden.", "port_987_bind_error": "Konnte sich nicht an Port 987 binden. Weitere Informationen findest du in der [Dokumentation] (https://www.home-assistant.io/components/ps4/).", "port_997_bind_error": "Bind to Port 997 nicht m\u00f6glich. Weitere Informationen findest du in der [Dokumentation](https://www.home-assistant.io/components/ps4/)" }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", "credential_timeout": "Zeit\u00fcberschreitung beim Warten auf den Anmeldedienst. Klicken zum Neustarten auf Senden.", - "login_failed": "Fehler beim Koppeln mit PlayStation 4. \u00dcberpr\u00fcfe, ob die PIN korrekt ist.", + "login_failed": "Fehler beim Koppeln mit der PlayStation 4. \u00dcberpr\u00fcfe, ob der PIN-Code korrekt ist.", "no_ipaddress": "Gib die IP-Adresse der PlayStation 4 ein, die konfiguriert werden soll." }, "step": { @@ -19,7 +20,7 @@ }, "link": { "data": { - "code": "PIN", + "code": "PIN-Code", "ip_address": "IP-Adresse", "name": "Name", "region": "Region" diff --git a/homeassistant/components/ps4/translations/tr.json b/homeassistant/components/ps4/translations/tr.json new file mode 100644 index 00000000000..4e3e0b53445 --- /dev/null +++ b/homeassistant/components/ps4/translations/tr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "link": { + "data": { + "ip_address": "\u0130p Adresi" + } + }, + "mode": { + "data": { + "ip_address": "\u0130p Adresi (Otomatik Bulma kullan\u0131l\u0131yorsa bo\u015f b\u0131rak\u0131n)." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ps4/translations/uk.json b/homeassistant/components/ps4/translations/uk.json new file mode 100644 index 00000000000..696a46bf8d7 --- /dev/null +++ b/homeassistant/components/ps4/translations/uk.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "credential_error": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u0456 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0445 \u0434\u0430\u043d\u0438\u0445.", + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "port_987_bind_error": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0456\u0437 \u043f\u043e\u0440\u0442\u043e\u043c 987. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438](https://www.home-assistant.io/components/ps4/).", + "port_997_bind_error": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0456\u0437 \u043f\u043e\u0440\u0442\u043e\u043c 997. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438](https://www.home-assistant.io/components/ps4/)." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "credential_timeout": "\u0427\u0430\u0441 \u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f \u043c\u0438\u043d\u0443\u0432. \u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c ** \u041f\u0406\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u0418 **, \u0449\u043e\u0431 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0438 \u0441\u043f\u0440\u043e\u0431\u0443.", + "login_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u043f\u0430\u0440\u0443 \u0437 PlayStation 4. \u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e PIN-\u043a\u043e\u0434 \u0432\u0432\u0435\u0434\u0435\u043d\u0438\u0439 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e.", + "no_ipaddress": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c IP-\u0430\u0434\u0440\u0435\u0441\u0443 PlayStation 4." + }, + "step": { + "creds": { + "description": "\u041d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c ** \u041f\u0406\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u0418 **, \u0430 \u043f\u043e\u0442\u0456\u043c \u0432 \u0434\u043e\u0434\u0430\u0442\u043a\u0443 'PS4 Second Screen' \u043e\u043d\u043e\u0432\u0456\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 \u0456 \u0432\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 'Home-Assistant'.", + "title": "PlayStation 4" + }, + "link": { + "data": { + "code": "PIN-\u043a\u043e\u0434", + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430", + "region": "\u0420\u0435\u0433\u0456\u043e\u043d" + }, + "description": "\u0414\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f PIN-\u043a\u043e\u0434\u0443 \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0434\u043e \u043f\u0443\u043d\u043a\u0442\u0443 ** \u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f ** \u043d\u0430 \u043a\u043e\u043d\u0441\u043e\u043b\u0456 PlayStation 4. \u041f\u043e\u0442\u0456\u043c \u0432\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 ** \u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u043c\u043e\u0431\u0456\u043b\u044c\u043d\u043e\u0433\u043e \u0434\u043e\u0434\u0430\u0442\u043a\u0430 ** \u0456 \u0432\u0438\u0431\u0435\u0440\u0456\u0442\u044c ** \u0414\u043e\u0434\u0430\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 ** . \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438](https://www.home-assistant.io/components/ps4/) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457.", + "title": "PlayStation 4" + }, + "mode": { + "data": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441\u0430 (\u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c \u043f\u0440\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u0456 \u0440\u0435\u0436\u0438\u043c\u0443 \u0430\u0432\u0442\u043e\u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f)", + "mode": "\u0420\u0435\u0436\u0438\u043c" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0442\u0438\u043f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u041f\u043e\u043b\u0435 'IP-\u0430\u0434\u0440\u0435\u0441\u0430' \u043c\u043e\u0436\u043d\u0430 \u0437\u0430\u043b\u0438\u0448\u0438\u0442\u0438 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c, \u044f\u043a\u0449\u043e \u0432\u0438\u0431\u0440\u0430\u043d\u043e 'Auto Discovery', \u043e\u0441\u043a\u0456\u043b\u044c\u043a\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u0431\u0443\u0434\u0443\u0442\u044c \u0434\u043e\u0434\u0430\u043d\u0456 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e.", + "title": "PlayStation 4" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ptvsd/__init__.py b/homeassistant/components/ptvsd/__init__.py deleted file mode 100644 index 258589084a0..00000000000 --- a/homeassistant/components/ptvsd/__init__.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -Enable ptvsd debugger to attach to HA. - -Attach ptvsd debugger by default to port 5678. -""" - -from asyncio import Event -import logging -from threading import Thread - -import voluptuous as vol - -from homeassistant.const import CONF_HOST, CONF_PORT -import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.typing import ConfigType, HomeAssistantType - -DOMAIN = "ptvsd" - -CONF_WAIT = "wait" - -_LOGGER = logging.getLogger(__name__) - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_HOST, default="0.0.0.0"): cv.string, - vol.Optional(CONF_PORT, default=5678): cv.port, - vol.Optional(CONF_WAIT, default=False): cv.boolean, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass: HomeAssistantType, config: ConfigType): - """Set up ptvsd debugger.""" - _LOGGER.warning( - "ptvsd is deprecated and will be removed in Home Assistant Core 0.120." - "The debugpy integration can be used as a full replacement for ptvsd" - ) - - # This is a local import, since importing this at the top, will cause - # ptvsd to hook into `sys.settrace`. So does `coverage` to generate - # coverage, resulting in a battle and incomplete code test coverage. - import ptvsd # pylint: disable=import-outside-toplevel - - conf = config[DOMAIN] - host = conf[CONF_HOST] - port = conf[CONF_PORT] - - ptvsd.enable_attach((host, port)) - - wait = conf[CONF_WAIT] - if wait: - _LOGGER.warning("Waiting for ptvsd connection on %s:%s", host, port) - ready = Event() - - def waitfor(): - ptvsd.wait_for_attach() - hass.loop.call_soon_threadsafe(ready.set) - - Thread(target=waitfor).start() - - await ready.wait() - else: - _LOGGER.warning("Listening for ptvsd connection on %s:%s", host, port) - - return True diff --git a/homeassistant/components/ptvsd/manifest.json b/homeassistant/components/ptvsd/manifest.json deleted file mode 100644 index 5feb04e92bb..00000000000 --- a/homeassistant/components/ptvsd/manifest.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "domain": "ptvsd", - "name": "PTVSD - Python Tools for Visual Studio Debug Server", - "documentation": "https://www.home-assistant.io/integrations/ptvsd", - "requirements": ["ptvsd==4.3.2"], - "codeowners": ["@swamp-ig"] -} diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/de.json b/homeassistant/components/pvpc_hourly_pricing/translations/de.json index 8e3e9b68e42..1b5c4d37658 100644 --- a/homeassistant/components/pvpc_hourly_pricing/translations/de.json +++ b/homeassistant/components/pvpc_hourly_pricing/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Die Integration ist bereits mit einem vorhandenen Sensor mit diesem Tarif konfiguriert" + "already_configured": "Der Dienst ist bereits konfiguriert" }, "step": { "user": { diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/tr.json b/homeassistant/components/pvpc_hourly_pricing/translations/tr.json new file mode 100644 index 00000000000..394f876401b --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/translations/tr.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "user": { + "data": { + "name": "Sens\u00f6r Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/uk.json b/homeassistant/components/pvpc_hourly_pricing/translations/uk.json new file mode 100644 index 00000000000..da2136d7765 --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430", + "tariff": "\u041a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u043d\u0438\u0439 \u0442\u0430\u0440\u0438\u0444 (1, 2 \u0430\u0431\u043e 3 \u043f\u0435\u0440\u0456\u043e\u0434\u0438)" + }, + "description": "\u0426\u0435\u0439 \u0441\u0435\u043d\u0441\u043e\u0440 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454 \u043e\u0444\u0456\u0446\u0456\u0439\u043d\u0438\u0439 API \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f [\u043f\u043e\u0433\u043e\u0434\u0438\u043d\u043d\u043e\u0457 \u0446\u0456\u043d\u0438 \u0437\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u0435\u043d\u0435\u0440\u0433\u0456\u044e (PVPC)] (https://www.esios.ree.es/es/pvpc) \u0432 \u0406\u0441\u043f\u0430\u043d\u0456\u0457.\n\u0414\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u0435\u0442\u0430\u043b\u044c\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044c \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\n\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0442\u0430\u0440\u0438\u0444, \u0437\u0430\u0441\u043d\u043e\u0432\u0430\u043d\u0438\u0439 \u043d\u0430 \u043a\u0456\u043b\u044c\u043a\u043e\u0441\u0442\u0456 \u0440\u043e\u0437\u0440\u0430\u0445\u0443\u043d\u043a\u043e\u0432\u0438\u0445 \u043f\u0435\u0440\u0456\u043e\u0434\u0456\u0432 \u0432 \u0434\u0435\u043d\u044c:\n- 1 \u043f\u0435\u0440\u0456\u043e\u0434: normal\n- 2 \u043f\u0435\u0440\u0456\u043e\u0434\u0438: discrimination (nightly rate)\n- 3 \u043f\u0435\u0440\u0456\u043e\u0434\u0438: electric car (nightly rate of 3 periods)", + "title": "\u0412\u0438\u0431\u0456\u0440 \u0442\u0430\u0440\u0438\u0444\u0443" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/python_script/__init__.py b/homeassistant/components/python_script/__init__.py index 6bd840074d9..bed159cae6b 100644 --- a/homeassistant/components/python_script/__init__.py +++ b/homeassistant/components/python_script/__init__.py @@ -23,7 +23,7 @@ from homeassistant.const import SERVICE_RELOAD from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.service import async_set_service_schema from homeassistant.loader import bind_hass -from homeassistant.util import sanitize_filename +from homeassistant.util import raise_if_invalid_filename import homeassistant.util.dt as dt_util from homeassistant.util.yaml.loader import load_yaml @@ -135,7 +135,8 @@ def discover_scripts(hass): def execute_script(hass, name, data=None): """Execute a script.""" filename = f"{name}.py" - with open(hass.config.path(FOLDER, sanitize_filename(filename))) as fil: + raise_if_invalid_filename(filename) + with open(hass.config.path(FOLDER, filename)) as fil: source = fil.read() execute(hass, filename, source, data) diff --git a/homeassistant/components/qrcode/manifest.json b/homeassistant/components/qrcode/manifest.json index 00d528ba399..b16eace14fd 100644 --- a/homeassistant/components/qrcode/manifest.json +++ b/homeassistant/components/qrcode/manifest.json @@ -2,6 +2,6 @@ "domain": "qrcode", "name": "QR Code", "documentation": "https://www.home-assistant.io/integrations/qrcode", - "requirements": ["pillow==7.2.0", "pyzbar==0.1.7"], + "requirements": ["pillow==8.1.0", "pyzbar==0.1.7"], "codeowners": [] } diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index 2c2918da4c9..672ff272344 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -5,21 +5,14 @@ import secrets from rachiopy import Rachio from requests.exceptions import ConnectTimeout -import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv -from .const import ( - CONF_CLOUDHOOK_URL, - CONF_MANUAL_RUN_MINS, - CONF_WEBHOOK_ID, - DEFAULT_MANUAL_RUN_MINS, - DOMAIN, -) +from .const import CONF_CLOUDHOOK_URL, CONF_MANUAL_RUN_MINS, CONF_WEBHOOK_ID, DOMAIN from .device import RachioPerson from .webhooks import ( async_get_or_create_registered_webhook_id_and_url, @@ -30,35 +23,14 @@ _LOGGER = logging.getLogger(__name__) SUPPORTED_DOMAINS = ["switch", "binary_sensor"] -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_API_KEY): cv.string, - vol.Optional( - CONF_MANUAL_RUN_MINS, default=DEFAULT_MANUAL_RUN_MINS - ): cv.positive_int, - } - ) - }, - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = cv.deprecated(DOMAIN) async def async_setup(hass: HomeAssistant, config: dict): """Set up the rachio component from YAML.""" - conf = config.get(DOMAIN) hass.data.setdefault(DOMAIN, {}) - if not conf: - return True - - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=conf - ) - ) return True diff --git a/homeassistant/components/rachio/config_flow.py b/homeassistant/components/rachio/config_flow.py index 96420d56ba7..63e5bd56954 100644 --- a/homeassistant/components/rachio/config_flow.py +++ b/homeassistant/components/rachio/config_flow.py @@ -94,10 +94,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(properties["id"]) return await self.async_step_user() - async def async_step_import(self, user_input): - """Handle import.""" - return await self.async_step_user(user_input) - @staticmethod @callback def async_get_options_flow(config_entry): diff --git a/homeassistant/components/rachio/manifest.json b/homeassistant/components/rachio/manifest.json index 224f59ea173..ba81b65b37f 100644 --- a/homeassistant/components/rachio/manifest.json +++ b/homeassistant/components/rachio/manifest.json @@ -7,6 +7,18 @@ "after_dependencies": ["cloud"], "codeowners": ["@bdraco"], "config_flow": true, + "dhcp": [{ + "hostname": "rachio-*", + "macaddress": "009D6B*" + }, + { + "hostname": "rachio-*", + "macaddress": "F0038C*" + }, + { + "hostname": "rachio-*", + "macaddress": "74C63B*" + }], "homekit": { "models": ["Rachio"] } diff --git a/homeassistant/components/rachio/translations/de.json b/homeassistant/components/rachio/translations/de.json index e6a4d73cde1..9acd92ce40d 100644 --- a/homeassistant/components/rachio/translations/de.json +++ b/homeassistant/components/rachio/translations/de.json @@ -4,7 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, @@ -13,7 +13,7 @@ "data": { "api_key": "API-Schl\u00fcssel" }, - "description": "Sie ben\u00f6tigen den API-Schl\u00fcssel von https://app.rach.io/. W\u00e4hlen Sie \"Kontoeinstellungen\" und klicken Sie dann auf \"API-SCHL\u00dcSSEL ERHALTEN\".", + "description": "Du ben\u00f6tigst den API-Schl\u00fcssel von https://app.rach.io/. Gehe in die Einstellungen und klicke auf \"API-SCHL\u00dcSSEL ANFORDERN\".", "title": "Stellen Sie eine Verbindung zu Ihrem Rachio-Ger\u00e4t her" } } @@ -22,7 +22,7 @@ "step": { "init": { "data": { - "manual_run_mins": "Wie lange, in Minuten, um eine Station einzuschalten, wenn der Schalter aktiviert ist." + "manual_run_mins": "Wie viele Minuten es laufen soll, wenn ein Zonen-Schalter aktiviert wird" } } } diff --git a/homeassistant/components/rachio/translations/tr.json b/homeassistant/components/rachio/translations/tr.json new file mode 100644 index 00000000000..8bbc4eb1e49 --- /dev/null +++ b/homeassistant/components/rachio/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rachio/translations/uk.json b/homeassistant/components/rachio/translations/uk.json new file mode 100644 index 00000000000..af5d7cd39d9 --- /dev/null +++ b/homeassistant/components/rachio/translations/uk.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API" + }, + "description": "\u0414\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u043e\u0442\u0440\u0456\u0431\u0435\u043d \u043a\u043b\u044e\u0447 API \u0437 \u0441\u0430\u0439\u0442\u0443 https://app.rach.io/. \u041f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0432 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f, \u0430 \u043f\u043e\u0442\u0456\u043c \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c 'GET API KEY'.", + "title": "Rachio" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "\u0422\u0440\u0438\u0432\u0430\u043b\u0456\u0441\u0442\u044c \u0440\u043e\u0431\u043e\u0442\u0438 \u043f\u0440\u0438 \u0430\u043a\u0442\u0438\u0432\u0430\u0446\u0456\u0457 \u043f\u0435\u0440\u0435\u043c\u0438\u043a\u0430\u0447\u0430 \u0437\u043e\u043d\u0438 (\u0432 \u0445\u0432\u0438\u043b\u0438\u043d\u0430\u0445)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/radiotherm/climate.py b/homeassistant/components/radiotherm/climate.py index 4383cf97a2d..f09ef95170f 100644 --- a/homeassistant/components/radiotherm/climate.py +++ b/homeassistant/components/radiotherm/climate.py @@ -1,5 +1,6 @@ """Support for Radio Thermostat wifi-enabled home thermostats.""" import logging +from socket import timeout import radiotherm import voluptuous as vol @@ -261,60 +262,60 @@ class RadioThermostat(ClimateEntity): # thermostats tend to time out sometimes when they're actively # heating or cooling. - # First time - get the name from the thermostat. This is - # normally set in the radio thermostat web app. - if self._name is None: - self._name = self.device.name["raw"] - - # Request the current state from the thermostat. try: + # First time - get the name from the thermostat. This is + # normally set in the radio thermostat web app. + if self._name is None: + self._name = self.device.name["raw"] + + # Request the current state from the thermostat. data = self.device.tstat["raw"] + + if self._is_model_ct80: + humiditydata = self.device.humidity["raw"] + except radiotherm.validate.RadiothermTstatError: _LOGGER.warning( "%s (%s) was busy (invalid value returned)", self._name, self.device.host, ) - return - current_temp = data["temp"] + except timeout: + _LOGGER.warning( + "Timeout waiting for response from %s (%s)", + self._name, + self.device.host, + ) - if self._is_model_ct80: - try: - humiditydata = self.device.humidity["raw"] - except radiotherm.validate.RadiothermTstatError: - _LOGGER.warning( - "%s (%s) was busy (invalid value returned)", - self._name, - self.device.host, - ) - return - self._current_humidity = humiditydata - self._program_mode = data["program_mode"] - self._preset_mode = CODE_TO_PRESET_MODE[data["program_mode"]] - - # Map thermostat values into various STATE_ flags. - self._current_temperature = current_temp - self._fmode = CODE_TO_FAN_MODE[data["fmode"]] - self._fstate = CODE_TO_FAN_STATE[data["fstate"]] - self._tmode = CODE_TO_TEMP_MODE[data["tmode"]] - self._tstate = CODE_TO_TEMP_STATE[data["tstate"]] - - self._current_operation = self._tmode - if self._tmode == HVAC_MODE_COOL: - self._target_temperature = data["t_cool"] - elif self._tmode == HVAC_MODE_HEAT: - self._target_temperature = data["t_heat"] - elif self._tmode == HVAC_MODE_AUTO: - # This doesn't really work - tstate is only set if the HVAC is - # active. If it's idle, we don't know what to do with the target - # temperature. - if self._tstate == CURRENT_HVAC_COOL: - self._target_temperature = data["t_cool"] - elif self._tstate == CURRENT_HVAC_HEAT: - self._target_temperature = data["t_heat"] else: - self._current_operation = HVAC_MODE_OFF + if self._is_model_ct80: + self._current_humidity = humiditydata + self._program_mode = data["program_mode"] + self._preset_mode = CODE_TO_PRESET_MODE[data["program_mode"]] + + # Map thermostat values into various STATE_ flags. + self._current_temperature = data["temp"] + self._fmode = CODE_TO_FAN_MODE[data["fmode"]] + self._fstate = CODE_TO_FAN_STATE[data["fstate"]] + self._tmode = CODE_TO_TEMP_MODE[data["tmode"]] + self._tstate = CODE_TO_TEMP_STATE[data["tstate"]] + + self._current_operation = self._tmode + if self._tmode == HVAC_MODE_COOL: + self._target_temperature = data["t_cool"] + elif self._tmode == HVAC_MODE_HEAT: + self._target_temperature = data["t_heat"] + elif self._tmode == HVAC_MODE_AUTO: + # This doesn't really work - tstate is only set if the HVAC is + # active. If it's idle, we don't know what to do with the target + # temperature. + if self._tstate == CURRENT_HVAC_COOL: + self._target_temperature = data["t_cool"] + elif self._tstate == CURRENT_HVAC_HEAT: + self._target_temperature = data["t_heat"] + else: + self._current_operation = HVAC_MODE_OFF def set_temperature(self, **kwargs): """Set new target temperature.""" diff --git a/homeassistant/components/radiotherm/manifest.json b/homeassistant/components/radiotherm/manifest.json index 6d4567c59d6..0220c233841 100644 --- a/homeassistant/components/radiotherm/manifest.json +++ b/homeassistant/components/radiotherm/manifest.json @@ -2,6 +2,6 @@ "domain": "radiotherm", "name": "Radio Thermostat", "documentation": "https://www.home-assistant.io/integrations/radiotherm", - "requirements": ["radiotherm==2.0.0"], - "codeowners": [] + "requirements": ["radiotherm==2.1.0"], + "codeowners": ["@vinnyfuria"] } diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index c8697adbcd4..98fbdbcf401 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -6,7 +6,6 @@ from functools import partial from regenmaschine import Client from regenmaschine.controller import Controller from regenmaschine.errors import RainMachineError -import voluptuous as vol from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( @@ -16,10 +15,9 @@ from homeassistant.const import ( CONF_PORT, CONF_SSL, ) -from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv -from homeassistant.helpers.service import verify_domain_control from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -35,15 +33,10 @@ from .const import ( DATA_RESTRICTIONS_CURRENT, DATA_RESTRICTIONS_UNIVERSAL, DATA_ZONES, - DEFAULT_ZONE_RUN, DOMAIN, LOGGER, ) -CONF_PROGRAM_ID = "program_id" -CONF_SECONDS = "seconds" -CONF_ZONE_ID = "zone_id" - DATA_LISTENER = "listener" DEFAULT_ATTRIBUTION = "Data provided by Green Electronics LLC" @@ -51,29 +44,6 @@ DEFAULT_ICON = "mdi:water" DEFAULT_SSL = True DEFAULT_UPDATE_INTERVAL = timedelta(seconds=15) -SERVICE_ALTER_PROGRAM = vol.Schema({vol.Required(CONF_PROGRAM_ID): cv.positive_int}) - -SERVICE_ALTER_ZONE = vol.Schema({vol.Required(CONF_ZONE_ID): cv.positive_int}) - -SERVICE_PAUSE_WATERING = vol.Schema({vol.Required(CONF_SECONDS): cv.positive_int}) - -SERVICE_START_PROGRAM_SCHEMA = vol.Schema( - {vol.Required(CONF_PROGRAM_ID): cv.positive_int} -) - -SERVICE_START_ZONE_SCHEMA = vol.Schema( - { - vol.Required(CONF_ZONE_ID): cv.positive_int, - vol.Optional(CONF_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN): cv.positive_int, - } -) - -SERVICE_STOP_PROGRAM_SCHEMA = vol.Schema( - {vol.Required(CONF_PROGRAM_ID): cv.positive_int} -) - -SERVICE_STOP_ZONE_SCHEMA = vol.Schema({vol.Required(CONF_ZONE_ID): cv.positive_int}) - CONFIG_SCHEMA = cv.deprecated(DOMAIN) PLATFORMS = ["binary_sensor", "sensor", "switch"] @@ -125,8 +95,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if entry_updates: hass.config_entries.async_update_entry(entry, **entry_updates) - _verify_domain_control = verify_domain_control(hass, DOMAIN) - websession = aiohttp_client.async_get_clientsession(hass) client = Client(session=websession) @@ -192,92 +160,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_forward_entry_setup(entry, component) ) - @_verify_domain_control - async def disable_program(call: ServiceCall): - """Disable a program.""" - await controller.programs.disable(call.data[CONF_PROGRAM_ID]) - await async_update_programs_and_zones(hass, entry) - - @_verify_domain_control - async def disable_zone(call: ServiceCall): - """Disable a zone.""" - await controller.zones.disable(call.data[CONF_ZONE_ID]) - await async_update_programs_and_zones(hass, entry) - - @_verify_domain_control - async def enable_program(call: ServiceCall): - """Enable a program.""" - await controller.programs.enable(call.data[CONF_PROGRAM_ID]) - await async_update_programs_and_zones(hass, entry) - - @_verify_domain_control - async def enable_zone(call: ServiceCall): - """Enable a zone.""" - await controller.zones.enable(call.data[CONF_ZONE_ID]) - await async_update_programs_and_zones(hass, entry) - - @_verify_domain_control - async def pause_watering(call: ServiceCall): - """Pause watering for a set number of seconds.""" - await controller.watering.pause_all(call.data[CONF_SECONDS]) - await async_update_programs_and_zones(hass, entry) - - @_verify_domain_control - async def start_program(call: ServiceCall): - """Start a particular program.""" - await controller.programs.start(call.data[CONF_PROGRAM_ID]) - await async_update_programs_and_zones(hass, entry) - - @_verify_domain_control - async def start_zone(call: ServiceCall): - """Start a particular zone for a certain amount of time.""" - await controller.zones.start( - call.data[CONF_ZONE_ID], call.data[CONF_ZONE_RUN_TIME] - ) - await async_update_programs_and_zones(hass, entry) - - @_verify_domain_control - async def stop_all(call: ServiceCall): - """Stop all watering.""" - await controller.watering.stop_all() - await async_update_programs_and_zones(hass, entry) - - @_verify_domain_control - async def stop_program(call: ServiceCall): - """Stop a program.""" - await controller.programs.stop(call.data[CONF_PROGRAM_ID]) - await async_update_programs_and_zones(hass, entry) - - @_verify_domain_control - async def stop_zone(call: ServiceCall): - """Stop a zone.""" - await controller.zones.stop(call.data[CONF_ZONE_ID]) - await async_update_programs_and_zones(hass, entry) - - @_verify_domain_control - async def unpause_watering(call: ServiceCall): - """Unpause watering.""" - await controller.watering.unpause_all() - await async_update_programs_and_zones(hass, entry) - - for service, method, schema in [ - ("disable_program", disable_program, SERVICE_ALTER_PROGRAM), - ("disable_zone", disable_zone, SERVICE_ALTER_ZONE), - ("enable_program", enable_program, SERVICE_ALTER_PROGRAM), - ("enable_zone", enable_zone, SERVICE_ALTER_ZONE), - ("pause_watering", pause_watering, SERVICE_PAUSE_WATERING), - ("start_program", start_program, SERVICE_START_PROGRAM_SCHEMA), - ("start_zone", start_zone, SERVICE_START_ZONE_SCHEMA), - ("stop_all", stop_all, {}), - ("stop_program", stop_program, SERVICE_STOP_PROGRAM_SCHEMA), - ("stop_zone", stop_zone, SERVICE_STOP_ZONE_SCHEMA), - ("unpause_watering", unpause_watering, {}), - ]: - hass.services.async_register(DOMAIN, service, method, schema=schema) - - hass.data[DOMAIN][DATA_LISTENER][entry.entry_id] = entry.add_update_listener( - async_reload_entry - ) + hass.data[DOMAIN][DATA_LISTENER] = entry.add_update_listener(async_reload_entry) return True diff --git a/homeassistant/components/rainmachine/services.yaml b/homeassistant/components/rainmachine/services.yaml index e66bd2a1d14..a73dc5c899d 100644 --- a/homeassistant/components/rainmachine/services.yaml +++ b/homeassistant/components/rainmachine/services.yaml @@ -2,42 +2,63 @@ disable_program: description: Disable a program. fields: + entity_id: + description: An entity from the desired RainMachine controller + example: switch.zone_1 program_id: description: The program to disable. example: 3 disable_zone: description: Disable a zone. fields: + entity_id: + description: An entity from the desired RainMachine controller + example: switch.zone_1 zone_id: description: The zone to disable. example: 3 enable_program: description: Enable a program. fields: + entity_id: + description: An entity from the desired RainMachine controller + example: switch.zone_1 program_id: description: The program to enable. example: 3 enable_zone: description: Enable a zone. fields: + entity_id: + description: An entity from the desired RainMachine controller + example: switch.zone_1 zone_id: description: The zone to enable. example: 3 pause_watering: description: Pause all watering for a number of seconds. fields: + entity_id: + description: An entity from the desired RainMachine controller + example: switch.zone_1 seconds: description: The number of seconds to pause. example: 30 start_program: description: Start a program. fields: + entity_id: + description: An entity from the desired RainMachine controller + example: switch.zone_1 program_id: description: The program to start. example: 3 start_zone: description: Start a zone for a set number of seconds. fields: + entity_id: + description: An entity from the desired RainMachine controller + example: switch.zone_1 zone_id: description: The zone to start. example: 3 @@ -46,17 +67,31 @@ start_zone: example: 120 stop_all: description: Stop all watering activities. + fields: + entity_id: + description: An entity from the desired RainMachine controller + example: switch.zone_1 stop_program: description: Stop a program. fields: + entity_id: + description: An entity from the desired RainMachine controller + example: switch.zone_1 program_id: description: The program to stop. example: 3 stop_zone: description: Stop a zone. fields: + entity_id: + description: An entity from the desired RainMachine controller + example: switch.zone_1 zone_id: description: The zone to stop. example: 3 unpause_watering: description: Unpause all watering. + fields: + entity_id: + description: An entity from the desired RainMachine controller + example: switch.zone_1 diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 5c54000a15f..6741abbfc9f 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -4,11 +4,13 @@ from typing import Callable, Coroutine from regenmaschine.controller import Controller from regenmaschine.errors import RequestError +import voluptuous as vol from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_ID from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import RainMachineEntity, async_update_programs_and_zones @@ -18,6 +20,7 @@ from .const import ( DATA_COORDINATOR, DATA_PROGRAMS, DATA_ZONES, + DEFAULT_ZONE_RUN, DOMAIN, LOGGER, ) @@ -43,6 +46,10 @@ ATTR_TIME_REMAINING = "time_remaining" ATTR_VEGETATION_TYPE = "vegetation_type" ATTR_ZONES = "zones" +CONF_PROGRAM_ID = "program_id" +CONF_SECONDS = "seconds" +CONF_ZONE_ID = "zone_id" + DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] RUN_STATUS_MAP = {0: "Not Running", 1: "Running", 2: "Queued"} @@ -103,6 +110,47 @@ async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities: Callable ) -> None: """Set up RainMachine switches based on a config entry.""" + platform = entity_platform.current_platform.get() + + alter_program_schema = {vol.Required(CONF_PROGRAM_ID): cv.positive_int} + alter_zone_schema = {vol.Required(CONF_ZONE_ID): cv.positive_int} + + for service_name, schema, method in [ + ("disable_program", alter_program_schema, "async_disable_program"), + ("disable_zone", alter_zone_schema, "async_disable_zone"), + ("enable_program", alter_program_schema, "async_enable_program"), + ("enable_zone", alter_zone_schema, "async_enable_zone"), + ( + "pause_watering", + {vol.Required(CONF_SECONDS): cv.positive_int}, + "async_pause_watering", + ), + ( + "start_program", + {vol.Required(CONF_PROGRAM_ID): cv.positive_int}, + "async_start_program", + ), + ( + "start_zone", + { + vol.Required(CONF_ZONE_ID): cv.positive_int, + vol.Optional( + CONF_ZONE_RUN_TIME, default=DEFAULT_ZONE_RUN + ): cv.positive_int, + }, + "async_start_zone", + ), + ("stop_all", {}, "async_stop_all"), + ( + "stop_program", + {vol.Required(CONF_PROGRAM_ID): cv.positive_int}, + "async_stop_program", + ), + ("stop_zone", {vol.Required(CONF_ZONE_ID): cv.positive_int}, "async_stop_zone"), + ("unpause_watering", {}, "async_unpause_watering"), + ]: + platform.async_register_entity_service(service_name, schema, method) + controller = hass.data[DOMAIN][DATA_CONTROLLER][entry.entry_id] programs_coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ DATA_PROGRAMS @@ -193,6 +241,61 @@ class RainMachineSwitch(RainMachineEntity, SwitchEntity): async_update_programs_and_zones(self.hass, self._entry) ) + async def async_disable_program(self, *, program_id): + """Disable a program.""" + await self._controller.programs.disable(program_id) + await async_update_programs_and_zones(self.hass, self._entry) + + async def async_disable_zone(self, *, zone_id): + """Disable a zone.""" + await self._controller.zones.disable(zone_id) + await async_update_programs_and_zones(self.hass, self._entry) + + async def async_enable_program(self, *, program_id): + """Enable a program.""" + await self._controller.programs.enable(program_id) + await async_update_programs_and_zones(self.hass, self._entry) + + async def async_enable_zone(self, *, zone_id): + """Enable a zone.""" + await self._controller.zones.enable(zone_id) + await async_update_programs_and_zones(self.hass, self._entry) + + async def async_pause_watering(self, *, seconds): + """Pause watering for a set number of seconds.""" + await self._controller.watering.pause_all(seconds) + await async_update_programs_and_zones(self.hass, self._entry) + + async def async_start_program(self, *, program_id): + """Start a particular program.""" + await self._controller.programs.start(program_id) + await async_update_programs_and_zones(self.hass, self._entry) + + async def async_start_zone(self, *, zone_id, zone_run_time): + """Start a particular zone for a certain amount of time.""" + await self._controller.zones.start(zone_id, zone_run_time) + await async_update_programs_and_zones(self.hass, self._entry) + + async def async_stop_all(self): + """Stop all watering.""" + await self._controller.watering.stop_all() + await async_update_programs_and_zones(self.hass, self._entry) + + async def async_stop_program(self, *, program_id): + """Stop a program.""" + await self._controller.programs.stop(program_id) + await async_update_programs_and_zones(self.hass, self._entry) + + async def async_stop_zone(self, *, zone_id): + """Stop a zone.""" + await self._controller.zones.stop(zone_id) + await async_update_programs_and_zones(self.hass, self._entry) + + async def async_unpause_watering(self): + """Unpause watering.""" + await self._controller.watering.unpause_all() + await async_update_programs_and_zones(self.hass, self._entry) + @callback def update_from_latest_data(self) -> None: """Update the state.""" diff --git a/homeassistant/components/rainmachine/translations/de.json b/homeassistant/components/rainmachine/translations/de.json index 92df52bb148..511d85b36b6 100644 --- a/homeassistant/components/rainmachine/translations/de.json +++ b/homeassistant/components/rainmachine/translations/de.json @@ -1,7 +1,10 @@ { "config": { "abort": { - "already_configured": "Dieser RainMachine-Kontroller ist bereits konfiguriert." + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { "user": { diff --git a/homeassistant/components/rainmachine/translations/tr.json b/homeassistant/components/rainmachine/translations/tr.json index 20f74cae994..80cfc05e568 100644 --- a/homeassistant/components/rainmachine/translations/tr.json +++ b/homeassistant/components/rainmachine/translations/tr.json @@ -1,4 +1,21 @@ { + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "ip_address": "Ana makine ad\u0131 veya IP adresi", + "password": "Parola", + "port": "Port" + } + } + } + }, "options": { "step": { "init": { diff --git a/homeassistant/components/rainmachine/translations/uk.json b/homeassistant/components/rainmachine/translations/uk.json new file mode 100644 index 00000000000..ff8d7089cec --- /dev/null +++ b/homeassistant/components/rainmachine/translations/uk.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "step": { + "user": { + "data": { + "ip_address": "\u0414\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "RainMachine" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "zone_run_time": "\u0427\u0430\u0441 \u0440\u043e\u0431\u043e\u0442\u0438 \u0437\u043e\u043d\u0438 \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f RainMachine" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/de.json b/homeassistant/components/recollect_waste/translations/de.json new file mode 100644 index 00000000000..7cbcea1b25e --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "step": { + "user": { + "data": { + "place_id": "Platz-ID", + "service_id": "Dienst-ID" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "Recollect Waste konfigurieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/es.json b/homeassistant/components/recollect_waste/translations/es.json index 2fdeb991bfd..69a39d435eb 100644 --- a/homeassistant/components/recollect_waste/translations/es.json +++ b/homeassistant/components/recollect_waste/translations/es.json @@ -20,7 +20,8 @@ "init": { "data": { "friendly_name": "Utilizar nombres descriptivos para los tipos de recogida (cuando sea posible)" - } + }, + "title": "Configurar la recogida de residuos" } } } diff --git a/homeassistant/components/recollect_waste/translations/lb.json b/homeassistant/components/recollect_waste/translations/lb.json new file mode 100644 index 00000000000..4e312bb0f23 --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/lb.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "invalid_place_or_service_id": "Ong\u00eblteg Place oder Service ID" + }, + "step": { + "user": { + "data": { + "place_id": "Place ID", + "service_id": "Service ID" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/pl.json b/homeassistant/components/recollect_waste/translations/pl.json index 013d0028790..cc0342e93d7 100644 --- a/homeassistant/components/recollect_waste/translations/pl.json +++ b/homeassistant/components/recollect_waste/translations/pl.json @@ -14,5 +14,15 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "U\u017cywaj przyjaznych nazw dla typu odbioru (je\u015bli to mo\u017cliwe)" + }, + "title": "Konfiguracja Recollect Waste" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/tr.json b/homeassistant/components/recollect_waste/translations/tr.json new file mode 100644 index 00000000000..5307276a71d --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recollect_waste/translations/uk.json b/homeassistant/components/recollect_waste/translations/uk.json new file mode 100644 index 00000000000..db47699f1ba --- /dev/null +++ b/homeassistant/components/recollect_waste/translations/uk.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "invalid_place_or_service_id": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 ID \u0440\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f \u0430\u0431\u043e \u0441\u043b\u0443\u0436\u0431\u0438." + }, + "step": { + "user": { + "data": { + "place_id": "ID \u0440\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f", + "service_id": "ID \u0441\u043b\u0443\u0436\u0431\u0438" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "friendly_name": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0437\u0440\u043e\u0437\u0443\u043c\u0456\u043b\u0456 \u0456\u043c\u0435\u043d\u0430 \u0434\u043b\u044f \u0442\u0438\u043f\u0456\u0432 \u0432\u0438\u0431\u043e\u0440\u0443 (\u044f\u043a\u0449\u043e \u043c\u043e\u0436\u043b\u0438\u0432\u043e)" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Recollect Waste" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/recorder/manifest.json b/homeassistant/components/recorder/manifest.json index 6d2311c76e7..67d3bdd0f5b 100644 --- a/homeassistant/components/recorder/manifest.json +++ b/homeassistant/components/recorder/manifest.json @@ -2,7 +2,7 @@ "domain": "recorder", "name": "Recorder", "documentation": "https://www.home-assistant.io/integrations/recorder", - "requirements": ["sqlalchemy==1.3.20"], + "requirements": ["sqlalchemy==1.3.22"], "codeowners": [], "quality_scale": "internal" } diff --git a/homeassistant/components/remember_the_milk/manifest.json b/homeassistant/components/remember_the_milk/manifest.json index cbbc385557c..f03f88023ae 100644 --- a/homeassistant/components/remember_the_milk/manifest.json +++ b/homeassistant/components/remember_the_milk/manifest.json @@ -2,7 +2,7 @@ "domain": "remember_the_milk", "name": "Remember The Milk", "documentation": "https://www.home-assistant.io/integrations/remember_the_milk", - "requirements": ["RtmAPI==0.7.2", "httplib2==0.10.3"], + "requirements": ["RtmAPI==0.7.2", "httplib2==0.18.1"], "dependencies": ["configurator"], "codeowners": [] } diff --git a/homeassistant/components/remote/translations/tr.json b/homeassistant/components/remote/translations/tr.json index cdc40c6268b..5359c99a78a 100644 --- a/homeassistant/components/remote/translations/tr.json +++ b/homeassistant/components/remote/translations/tr.json @@ -1,4 +1,14 @@ { + "device_automation": { + "action_type": { + "turn_off": "{entity_name} kapat", + "turn_on": "{entity_name} a\u00e7\u0131n" + }, + "trigger_type": { + "turned_off": "{entity_name} kapat\u0131ld\u0131", + "turned_on": "{entity_name} a\u00e7\u0131ld\u0131" + } + }, "state": { "_": { "off": "Kapal\u0131", diff --git a/homeassistant/components/remote/translations/uk.json b/homeassistant/components/remote/translations/uk.json index 2feda4928e5..1f275f5f2eb 100644 --- a/homeassistant/components/remote/translations/uk.json +++ b/homeassistant/components/remote/translations/uk.json @@ -1,5 +1,14 @@ { "device_automation": { + "action_type": { + "toggle": "{entity_name}: \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u0438", + "turn_off": "{entity_name}: \u0432\u0438\u043c\u043a\u043d\u0443\u0442\u0438", + "turn_on": "{entity_name}: \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438" + }, + "condition_type": { + "is_off": "{entity_name} \u0443 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_on": "{entity_name} \u0443 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456" + }, "trigger_type": { "turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e", "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u043e" diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index 68b6d841654..116b2464213 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -520,7 +520,7 @@ class RflinkCommand(RflinkDevice): if self._wait_ack: # Puts command on outgoing buffer then waits for Rflink to confirm - # the command has been send out in the ether. + # the command has been sent out. await self._protocol.send_command_ack(self._device_id, cmd) else: # Puts command on outgoing buffer and returns straight away. diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 9eaa705bf3e..067ffeb5313 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -12,6 +12,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.binary_sensor import DEVICE_CLASSES_SCHEMA from homeassistant.const import ( + ATTR_DEVICE_ID, CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_DEVICE, @@ -37,6 +38,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceRegistry from homeassistant.helpers.restore_state import RestoreEntity from .const import ( @@ -275,6 +277,10 @@ async def async_setup_internal(hass, entry: config_entries.ConfigEntry): # Setup some per device config devices = _get_device_lookup(config[CONF_DEVICES]) + device_registry: DeviceRegistry = ( + await hass.helpers.device_registry.async_get_registry() + ) + # Declare the Handle event @callback def async_handle_receive(event): @@ -297,9 +303,17 @@ async def async_setup_internal(hass, entry: config_entries.ConfigEntry): data_bits = get_device_data_bits(event.device, devices) device_id = get_device_id(event.device, data_bits=data_bits) - # Register new devices - if config[CONF_AUTOMATIC_ADD] and device_id not in devices: - _add_device(event, device_id) + if device_id not in devices: + if config[CONF_AUTOMATIC_ADD]: + _add_device(event, device_id) + else: + return + + device_entry = device_registry.async_get_device( + identifiers={(DOMAIN, *device_id)}, + ) + if device_entry: + event_data[ATTR_DEVICE_ID] = device_entry.id # Callback to HA registered components. hass.helpers.dispatcher.async_dispatcher_send(SIGNAL_EVENT, event, device_id) diff --git a/homeassistant/components/rfxtrx/config_flow.py b/homeassistant/components/rfxtrx/config_flow.py index f81fdde3c4c..5eeb9b38411 100644 --- a/homeassistant/components/rfxtrx/config_flow.py +++ b/homeassistant/components/rfxtrx/config_flow.py @@ -40,6 +40,10 @@ from .const import ( CONF_REMOVE_DEVICE, CONF_REPLACE_DEVICE, CONF_SIGNAL_REPETITIONS, + CONF_VENETIAN_BLIND_MODE, + CONST_VENETIAN_BLIND_MODE_DEFAULT, + CONST_VENETIAN_BLIND_MODE_EU, + CONST_VENETIAN_BLIND_MODE_US, DEVICE_PACKET_TYPE_LIGHTING4, ) from .cover import supported as cover_supported @@ -218,6 +222,10 @@ class OptionsFlow(config_entries.OptionsFlow): device[CONF_COMMAND_ON] = command_on if command_off: device[CONF_COMMAND_OFF] = command_off + if user_input.get(CONF_VENETIAN_BLIND_MODE): + device[CONF_VENETIAN_BLIND_MODE] = user_input[ + CONF_VENETIAN_BLIND_MODE + ] self.update_config_data( global_options=self._global_options, devices=devices @@ -282,6 +290,23 @@ class OptionsFlow(config_entries.OptionsFlow): } ) + if isinstance(self._selected_device_object.device, rfxtrxmod.RfyDevice): + data_schema.update( + { + vol.Optional( + CONF_VENETIAN_BLIND_MODE, + default=device_data.get( + CONF_VENETIAN_BLIND_MODE, CONST_VENETIAN_BLIND_MODE_DEFAULT + ), + ): vol.In( + [ + CONST_VENETIAN_BLIND_MODE_DEFAULT, + CONST_VENETIAN_BLIND_MODE_US, + CONST_VENETIAN_BLIND_MODE_EU, + ] + ), + } + ) devices = { entry.id: entry.name_by_user if entry.name_by_user else entry.name for entry in self._device_entries diff --git a/homeassistant/components/rfxtrx/const.py b/homeassistant/components/rfxtrx/const.py index 28aec125644..1f36b00e184 100644 --- a/homeassistant/components/rfxtrx/const.py +++ b/homeassistant/components/rfxtrx/const.py @@ -6,10 +6,15 @@ CONF_AUTOMATIC_ADD = "automatic_add" CONF_SIGNAL_REPETITIONS = "signal_repetitions" CONF_DEBUG = "debug" CONF_OFF_DELAY = "off_delay" +CONF_VENETIAN_BLIND_MODE = "venetian_blind_mode" CONF_REMOVE_DEVICE = "remove_device" CONF_REPLACE_DEVICE = "replace_device" +CONST_VENETIAN_BLIND_MODE_DEFAULT = "Unknown" +CONST_VENETIAN_BLIND_MODE_EU = "EU" +CONST_VENETIAN_BLIND_MODE_US = "US" + COMMAND_ON_LIST = [ "On", "Up", diff --git a/homeassistant/components/rfxtrx/cover.py b/homeassistant/components/rfxtrx/cover.py index dfbaa60f589..a5f5edd0e42 100644 --- a/homeassistant/components/rfxtrx/cover.py +++ b/homeassistant/components/rfxtrx/cover.py @@ -1,7 +1,15 @@ """Support for RFXtrx covers.""" import logging -from homeassistant.components.cover import CoverEntity +from homeassistant.components.cover import ( + SUPPORT_CLOSE, + SUPPORT_CLOSE_TILT, + SUPPORT_OPEN, + SUPPORT_OPEN_TILT, + SUPPORT_STOP, + SUPPORT_STOP_TILT, + CoverEntity, +) from homeassistant.const import CONF_DEVICES, STATE_OPEN from homeassistant.core import callback @@ -14,7 +22,13 @@ from . import ( get_device_id, get_rfx_object, ) -from .const import COMMAND_OFF_LIST, COMMAND_ON_LIST +from .const import ( + COMMAND_OFF_LIST, + COMMAND_ON_LIST, + CONF_VENETIAN_BLIND_MODE, + CONST_VENETIAN_BLIND_MODE_EU, + CONST_VENETIAN_BLIND_MODE_US, +) _LOGGER = logging.getLogger(__name__) @@ -50,7 +64,10 @@ async def async_setup_entry( device_ids.add(device_id) entity = RfxtrxCover( - event.device, device_id, entity_info[CONF_SIGNAL_REPETITIONS] + event.device, + device_id, + signal_repetitions=entity_info[CONF_SIGNAL_REPETITIONS], + venetian_blind_mode=entity_info.get(CONF_VENETIAN_BLIND_MODE), ) entities.append(entity) @@ -86,6 +103,18 @@ async def async_setup_entry( class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): """Representation of a RFXtrx cover.""" + def __init__( + self, + device, + device_id, + signal_repetitions, + event=None, + venetian_blind_mode=None, + ): + """Initialize the RFXtrx cover device.""" + super().__init__(device, device_id, signal_repetitions, event) + self._venetian_blind_mode = venetian_blind_mode + async def async_added_to_hass(self): """Restore device state.""" await super().async_added_to_hass() @@ -95,6 +124,21 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): if old_state is not None: self._state = old_state.state == STATE_OPEN + @property + def supported_features(self): + """Flag supported features.""" + supported_features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_STOP + + if self._venetian_blind_mode in ( + CONST_VENETIAN_BLIND_MODE_US, + CONST_VENETIAN_BLIND_MODE_EU, + ): + supported_features |= ( + SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_STOP_TILT + ) + + return supported_features + @property def is_closed(self): """Return if the cover is closed.""" @@ -102,13 +146,23 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): async def async_open_cover(self, **kwargs): """Move the cover up.""" - await self._async_send(self._device.send_open) + if self._venetian_blind_mode == CONST_VENETIAN_BLIND_MODE_US: + await self._async_send(self._device.send_up05sec) + elif self._venetian_blind_mode == CONST_VENETIAN_BLIND_MODE_EU: + await self._async_send(self._device.send_up2sec) + else: + await self._async_send(self._device.send_open) self._state = True self.async_write_ha_state() async def async_close_cover(self, **kwargs): """Move the cover down.""" - await self._async_send(self._device.send_close) + if self._venetian_blind_mode == CONST_VENETIAN_BLIND_MODE_US: + await self._async_send(self._device.send_down05sec) + elif self._venetian_blind_mode == CONST_VENETIAN_BLIND_MODE_EU: + await self._async_send(self._device.send_down2sec) + else: + await self._async_send(self._device.send_close) self._state = False self.async_write_ha_state() @@ -118,6 +172,26 @@ class RfxtrxCover(RfxtrxCommandEntity, CoverEntity): self._state = True self.async_write_ha_state() + async def async_open_cover_tilt(self, **kwargs): + """Tilt the cover up.""" + if self._venetian_blind_mode == CONST_VENETIAN_BLIND_MODE_US: + await self._async_send(self._device.send_up2sec) + elif self._venetian_blind_mode == CONST_VENETIAN_BLIND_MODE_EU: + await self._async_send(self._device.send_up05sec) + + async def async_close_cover_tilt(self, **kwargs): + """Tilt the cover down.""" + if self._venetian_blind_mode == CONST_VENETIAN_BLIND_MODE_US: + await self._async_send(self._device.send_down2sec) + elif self._venetian_blind_mode == CONST_VENETIAN_BLIND_MODE_EU: + await self._async_send(self._device.send_down05sec) + + async def async_stop_cover_tilt(self, **kwargs): + """Stop the cover tilt.""" + await self._async_send(self._device.send_stop) + self._state = True + self.async_write_ha_state() + def _apply_event(self, event): """Apply command from rfxtrx.""" super()._apply_event(event) diff --git a/homeassistant/components/rfxtrx/manifest.json b/homeassistant/components/rfxtrx/manifest.json index e62fc5c3c83..19e834d11d6 100644 --- a/homeassistant/components/rfxtrx/manifest.json +++ b/homeassistant/components/rfxtrx/manifest.json @@ -2,7 +2,7 @@ "domain": "rfxtrx", "name": "RFXCOM RFXtrx", "documentation": "https://www.home-assistant.io/integrations/rfxtrx", - "requirements": ["pyRFXtrx==0.26"], + "requirements": ["pyRFXtrx==0.26.1"], "codeowners": ["@danielhiversen", "@elupus", "@RobBie1221"], "config_flow": true } diff --git a/homeassistant/components/rfxtrx/strings.json b/homeassistant/components/rfxtrx/strings.json index 574cff29ba1..c89fcddb002 100644 --- a/homeassistant/components/rfxtrx/strings.json +++ b/homeassistant/components/rfxtrx/strings.json @@ -56,6 +56,7 @@ "command_on": "Data bits value for command on", "command_off": "Data bits value for command off", "signal_repetitions": "Number of signal repetitions", + "venetian_blind_mode": "Venetian blind mode", "replace_device": "Select device to replace" }, "title": "Configure device options" diff --git a/homeassistant/components/rfxtrx/translations/ca.json b/homeassistant/components/rfxtrx/translations/ca.json index 6c4e920df02..d7db4107e3b 100644 --- a/homeassistant/components/rfxtrx/translations/ca.json +++ b/homeassistant/components/rfxtrx/translations/ca.json @@ -64,7 +64,8 @@ "off_delay": "Retard OFF", "off_delay_enabled": "Activa el retard OFF", "replace_device": "Selecciona el dispositiu a substituir", - "signal_repetitions": "Nombre de repeticions del senyal" + "signal_repetitions": "Nombre de repeticions del senyal", + "venetian_blind_mode": "Mode persiana veneciana" }, "title": "Configuraci\u00f3 de les opcions del dispositiu" } diff --git a/homeassistant/components/rfxtrx/translations/de.json b/homeassistant/components/rfxtrx/translations/de.json index 1979a10cb8a..b1e4197c0f1 100644 --- a/homeassistant/components/rfxtrx/translations/de.json +++ b/homeassistant/components/rfxtrx/translations/de.json @@ -2,13 +2,17 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert. Nur eine Konfiguration m\u00f6glich.", - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "setup_network": { + "data": { + "host": "Host", + "port": "Port" + }, "title": "Verbindungsadresse ausw\u00e4hlen" }, "setup_serial": { @@ -18,6 +22,9 @@ "title": "Ger\u00e4t" }, "setup_serial_manual_path": { + "data": { + "device": "USB-Ger\u00e4te-Pfad" + }, "title": "Pfad" }, "user": { @@ -30,6 +37,7 @@ }, "options": { "error": { + "already_configured_device": "Ger\u00e4t ist bereits konfiguriert", "unknown": "Unerwarteter Fehler" }, "step": { @@ -37,6 +45,13 @@ "data": { "debug": "Debugging aktivieren" } + }, + "set_device_options": { + "data": { + "off_delay": "Ausschaltverz\u00f6gerung", + "off_delay_enabled": "Ausschaltverz\u00f6gerung aktivieren", + "replace_device": "W\u00e4hle ein Ger\u00e4t aus, das ersetzt werden soll" + } } } } diff --git a/homeassistant/components/rfxtrx/translations/en.json b/homeassistant/components/rfxtrx/translations/en.json index 3f8fcf12702..5e3f551e0cf 100644 --- a/homeassistant/components/rfxtrx/translations/en.json +++ b/homeassistant/components/rfxtrx/translations/en.json @@ -64,7 +64,8 @@ "off_delay": "Off delay", "off_delay_enabled": "Enable off delay", "replace_device": "Select device to replace", - "signal_repetitions": "Number of signal repetitions" + "signal_repetitions": "Number of signal repetitions", + "venetian_blind_mode": "Venetian blind mode" }, "title": "Configure device options" } diff --git a/homeassistant/components/rfxtrx/translations/es.json b/homeassistant/components/rfxtrx/translations/es.json index 86bad8f096f..c1c4d72735c 100644 --- a/homeassistant/components/rfxtrx/translations/es.json +++ b/homeassistant/components/rfxtrx/translations/es.json @@ -64,7 +64,8 @@ "off_delay": "Retraso de apagado", "off_delay_enabled": "Activar retardo de apagado", "replace_device": "Seleccione el dispositivo que desea reemplazar", - "signal_repetitions": "N\u00famero de repeticiones de la se\u00f1al" + "signal_repetitions": "N\u00famero de repeticiones de la se\u00f1al", + "venetian_blind_mode": "Modo de persiana veneciana" }, "title": "Configurar las opciones del dispositivo" } diff --git a/homeassistant/components/rfxtrx/translations/et.json b/homeassistant/components/rfxtrx/translations/et.json index 1ade1f112c2..662664b4454 100644 --- a/homeassistant/components/rfxtrx/translations/et.json +++ b/homeassistant/components/rfxtrx/translations/et.json @@ -64,7 +64,8 @@ "off_delay": "V\u00e4ljal\u00fclitamise viivitus", "off_delay_enabled": "Luba v\u00e4ljal\u00fclitusviivitus", "replace_device": "Vali asendav seade", - "signal_repetitions": "Signaali korduste arv" + "signal_repetitions": "Signaali korduste arv", + "venetian_blind_mode": "Ribikardinate juhtimine" }, "title": "Seadista seadme valikud" } diff --git a/homeassistant/components/rfxtrx/translations/it.json b/homeassistant/components/rfxtrx/translations/it.json index ff705fdd0a2..938c471e992 100644 --- a/homeassistant/components/rfxtrx/translations/it.json +++ b/homeassistant/components/rfxtrx/translations/it.json @@ -64,7 +64,8 @@ "off_delay": "Ritardo di spegnimento", "off_delay_enabled": "Attivare il ritardo di spegnimento", "replace_device": "Selezionare il dispositivo da sostituire", - "signal_repetitions": "Numero di ripetizioni del segnale" + "signal_repetitions": "Numero di ripetizioni del segnale", + "venetian_blind_mode": "Modalit\u00e0 veneziana" }, "title": "Configurare le opzioni del dispositivo" } diff --git a/homeassistant/components/rfxtrx/translations/no.json b/homeassistant/components/rfxtrx/translations/no.json index 752136dac7f..3eb9c9b83df 100644 --- a/homeassistant/components/rfxtrx/translations/no.json +++ b/homeassistant/components/rfxtrx/translations/no.json @@ -64,7 +64,8 @@ "off_delay": "Av forsinkelse", "off_delay_enabled": "Aktiver av forsinkelse", "replace_device": "Velg enheten du vil erstatte", - "signal_repetitions": "Antall signalrepetisjoner" + "signal_repetitions": "Antall signalrepetisjoner", + "venetian_blind_mode": "Persiennemodus" }, "title": "Konfigurer enhetsalternativer" } diff --git a/homeassistant/components/rfxtrx/translations/pl.json b/homeassistant/components/rfxtrx/translations/pl.json index bf17d6c5166..e0e69b2a64a 100644 --- a/homeassistant/components/rfxtrx/translations/pl.json +++ b/homeassistant/components/rfxtrx/translations/pl.json @@ -75,7 +75,8 @@ "off_delay": "Op\u00f3\u017anienie stanu \"off\"", "off_delay_enabled": "W\u0142\u0105cz op\u00f3\u017anienie stanu \"off\"", "replace_device": "Wybierz urz\u0105dzenie do zast\u0105pienia", - "signal_repetitions": "Liczba powt\u00f3rze\u0144 sygna\u0142u" + "signal_repetitions": "Liczba powt\u00f3rze\u0144 sygna\u0142u", + "venetian_blind_mode": "Tryb \u017caluzji weneckich" }, "title": "Konfiguracja opcji urz\u0105dzenia" } diff --git a/homeassistant/components/rfxtrx/translations/ru.json b/homeassistant/components/rfxtrx/translations/ru.json index 361b051ac2c..5a635766d3f 100644 --- a/homeassistant/components/rfxtrx/translations/ru.json +++ b/homeassistant/components/rfxtrx/translations/ru.json @@ -64,7 +64,8 @@ "off_delay": "\u0417\u0430\u0434\u0435\u0440\u0436\u043a\u0430 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", "off_delay_enabled": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0437\u0430\u0434\u0435\u0440\u0436\u043a\u0443 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", "replace_device": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0434\u043b\u044f \u0437\u0430\u043c\u0435\u043d\u044b", - "signal_repetitions": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0442\u043e\u0440\u043e\u0432 \u0441\u0438\u0433\u043d\u0430\u043b\u0430" + "signal_repetitions": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0442\u043e\u0440\u043e\u0432 \u0441\u0438\u0433\u043d\u0430\u043b\u0430", + "venetian_blind_mode": "\u0420\u0435\u0436\u0438\u043c \u0432\u0435\u043d\u0435\u0446\u0438\u0430\u043d\u0441\u043a\u0438\u0445 \u0436\u0430\u043b\u044e\u0437\u0438" }, "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430" } diff --git a/homeassistant/components/rfxtrx/translations/tr.json b/homeassistant/components/rfxtrx/translations/tr.json new file mode 100644 index 00000000000..1c3ad8b9e05 --- /dev/null +++ b/homeassistant/components/rfxtrx/translations/tr.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "setup_network": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + } + } + } + }, + "options": { + "error": { + "already_configured_device": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "unknown": "Beklenmeyen hata" + }, + "step": { + "set_device_options": { + "data": { + "venetian_blind_mode": "Jaluzi modu" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/uk.json b/homeassistant/components/rfxtrx/translations/uk.json new file mode 100644 index 00000000000..1b0938b8b70 --- /dev/null +++ b/homeassistant/components/rfxtrx/translations/uk.json @@ -0,0 +1,74 @@ +{ + "config": { + "abort": { + "already_configured": "\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.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "setup_network": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f" + }, + "setup_serial": { + "data": { + "device": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + }, + "title": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + }, + "setup_serial_manual_path": { + "data": { + "device": "\u0428\u043b\u044f\u0445 \u0434\u043e USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + }, + "title": "\u0428\u043b\u044f\u0445" + }, + "user": { + "data": { + "type": "\u0422\u0438\u043f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f" + }, + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0442\u0438\u043f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f" + } + } + }, + "options": { + "error": { + "already_configured_device": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "invalid_event_code": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043a\u043e\u0434 \u043f\u043e\u0434\u0456\u0457.", + "invalid_input_2262_off": "\u041d\u0435\u0432\u0456\u0440\u043d\u0456 \u0434\u0430\u043d\u0456 \u0434\u043b\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u0438 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043d\u044f.", + "invalid_input_2262_on": "\u041d\u0435\u0432\u0456\u0440\u043d\u0456 \u0434\u0430\u043d\u0456 \u0434\u043b\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u0438 \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043d\u044f.", + "invalid_input_off_delay": "\u041d\u0435\u0432\u0456\u0440\u043d\u0456 \u0434\u0430\u043d\u0456 \u0434\u043b\u044f \u0437\u0430\u0442\u0440\u0438\u043c\u043a\u0438 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043d\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "prompt_options": { + "data": { + "automatic_add": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0435 \u0434\u043e\u0434\u0430\u0432\u0430\u043d\u043d\u044f", + "debug": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0440\u0435\u0436\u0438\u043c \u043d\u0430\u043b\u0430\u0433\u043e\u0434\u0436\u0435\u043d\u043d\u044f", + "device": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0434\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f", + "event_code": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043a\u043e\u0434 \u043f\u043e\u0434\u0456\u0457", + "remove_device": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0434\u043b\u044f \u0432\u0438\u0434\u0430\u043b\u0435\u043d\u043d\u044f" + }, + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438" + }, + "set_device_options": { + "data": { + "command_off": "\u0417\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0431\u0456\u0442\u0456\u0432 \u0434\u0430\u043d\u0438\u0445 \u0434\u043b\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u0438 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043d\u044f", + "command_on": "\u0417\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0431\u0456\u0442\u0456\u0432 \u0434\u0430\u043d\u0438\u0445 \u0434\u043b\u044f \u043a\u043e\u043c\u0430\u043d\u0434\u0438 \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043d\u044f", + "data_bit": "\u041a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u0431\u0456\u0442\u0456\u0432 \u0434\u0430\u043d\u0438\u0445", + "fire_event": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u043f\u043e\u0434\u0456\u0457 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e", + "off_delay": "\u0417\u0430\u0442\u0440\u0438\u043c\u043a\u0430 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043d\u044f", + "off_delay_enabled": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u0437\u0430\u0442\u0440\u0438\u043c\u043a\u0443 \u0432\u0438\u043c\u0438\u043a\u0430\u043d\u043d\u044f", + "replace_device": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0434\u043b\u044f \u0437\u0430\u043c\u0456\u043d\u0438", + "signal_repetitions": "\u041a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u043f\u043e\u0432\u0442\u043e\u0440\u0456\u0432 \u0441\u0438\u0433\u043d\u0430\u043b\u0443", + "venetian_blind_mode": "\u0420\u0435\u0436\u0438\u043c \u0436\u0430\u043b\u044e\u0437\u0456" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rfxtrx/translations/zh-Hant.json b/homeassistant/components/rfxtrx/translations/zh-Hant.json index 3da2e5f5384..24e5ee56d76 100644 --- a/homeassistant/components/rfxtrx/translations/zh-Hant.json +++ b/homeassistant/components/rfxtrx/translations/zh-Hant.json @@ -64,7 +64,8 @@ "off_delay": "\u5ef6\u9072", "off_delay_enabled": "\u958b\u555f\u5ef6\u9072", "replace_device": "\u9078\u64c7\u88dd\u7f6e\u4ee5\u53d6\u4ee3", - "signal_repetitions": "\u8a0a\u865f\u91cd\u8907\u6b21\u6578" + "signal_repetitions": "\u8a0a\u865f\u91cd\u8907\u6b21\u6578", + "venetian_blind_mode": "\u767e\u8449\u7a97\u6a21\u5f0f" }, "title": "\u8a2d\u5b9a\u88dd\u7f6e\u9078\u9805" } diff --git a/homeassistant/components/ring/manifest.json b/homeassistant/components/ring/manifest.json index 550da4d38ec..38083830311 100644 --- a/homeassistant/components/ring/manifest.json +++ b/homeassistant/components/ring/manifest.json @@ -5,5 +5,6 @@ "requirements": ["ring_doorbell==0.6.2"], "dependencies": ["ffmpeg"], "codeowners": ["@balloob"], - "config_flow": true + "config_flow": true, + "dhcp": [{"hostname":"ring*","macaddress":"0CAE7D*"}] } diff --git a/homeassistant/components/ring/translations/tr.json b/homeassistant/components/ring/translations/tr.json new file mode 100644 index 00000000000..caba385d7fa --- /dev/null +++ b/homeassistant/components/ring/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ring/translations/uk.json b/homeassistant/components/ring/translations/uk.json new file mode 100644 index 00000000000..8d40cdf0d23 --- /dev/null +++ b/homeassistant/components/ring/translations/uk.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "2fa": { + "data": { + "2fa": "\u041a\u043e\u0434 \u0434\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0457 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + }, + "title": "\u0414\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "Ring" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index 3fb8f19a1db..685fee43adf 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -60,10 +60,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): EVENTS_COORDINATOR: events_coordinator, } - for component in PLATFORMS: - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(entry, component) + async def start_platforms(): + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_setup(entry, component) + for component in PLATFORMS + ] ) + await events_coordinator.async_refresh() + + hass.async_create_task(start_platforms()) return True diff --git a/homeassistant/components/risco/sensor.py b/homeassistant/components/risco/sensor.py index 62ef6643551..43d763a35fa 100644 --- a/homeassistant/components/risco/sensor.py +++ b/homeassistant/components/risco/sensor.py @@ -73,7 +73,6 @@ class RiscoSensor(CoordinatorEntity): self.async_on_remove( self.coordinator.async_add_listener(self._refresh_from_coordinator) ) - await self.coordinator.async_request_refresh() def _refresh_from_coordinator(self): events = self.coordinator.data diff --git a/homeassistant/components/risco/translations/de.json b/homeassistant/components/risco/translations/de.json index ad863f7ff79..36d808bd6de 100644 --- a/homeassistant/components/risco/translations/de.json +++ b/homeassistant/components/risco/translations/de.json @@ -1,14 +1,18 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { "user": { "data": { "password": "Passwort", - "pin": "PIN Code", + "pin": "PIN-Code", "username": "Benutzername" } } @@ -16,6 +20,12 @@ }, "options": { "step": { + "init": { + "data": { + "code_arm_required": "PIN-Code zum Entsperren vorgeben", + "code_disarm_required": "PIN-Code zum Entsperren vorgeben" + } + }, "risco_to_ha": { "data": { "A": "Gruppe A", diff --git a/homeassistant/components/risco/translations/lb.json b/homeassistant/components/risco/translations/lb.json index 197dd78c403..ae136cb1843 100644 --- a/homeassistant/components/risco/translations/lb.json +++ b/homeassistant/components/risco/translations/lb.json @@ -39,7 +39,9 @@ "A": "Grupp A", "B": "Grupp B", "C": "Grupp C", - "D": "Grupp D" + "D": "Grupp D", + "arm": "Aktiv\u00e9iert (\u00cbNNERWEE)", + "partial_arm": "Deelweis Aktiv\u00e9iert (DOHEEM)" } } } diff --git a/homeassistant/components/risco/translations/tr.json b/homeassistant/components/risco/translations/tr.json new file mode 100644 index 00000000000..02a3b505f84 --- /dev/null +++ b/homeassistant/components/risco/translations/tr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "options": { + "step": { + "init": { + "title": "Se\u00e7enekleri yap\u0131land\u0131r\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/risco/translations/uk.json b/homeassistant/components/risco/translations/uk.json new file mode 100644 index 00000000000..53b64344f2e --- /dev/null +++ b/homeassistant/components/risco/translations/uk.json @@ -0,0 +1,55 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "pin": "PIN-\u043a\u043e\u0434", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + }, + "options": { + "step": { + "ha_to_risco": { + "data": { + "armed_away": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 (\u041d\u0435 \u0432\u0434\u043e\u043c\u0430)", + "armed_custom_bypass": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 \u0437 \u0432\u0438\u043d\u044f\u0442\u043a\u0430\u043c\u0438", + "armed_home": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 (\u0412\u0434\u043e\u043c\u0430)", + "armed_night": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 (\u043d\u0456\u0447)" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0442\u0430\u043d \u0441\u0438\u0433\u043d\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u0457 Risco \u043f\u0440\u0438 \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043d\u0456 \u0441\u0438\u0433\u043d\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u0457 Home Assistant", + "title": "\u0417\u0456\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044f \u0441\u0442\u0430\u043d\u0456\u0432 Home Assistant \u0456 Risco" + }, + "init": { + "data": { + "code_arm_required": "\u0412\u0438\u043c\u0430\u0433\u0430\u0442\u0438 PIN-\u043a\u043e\u0434 \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u043d\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0443", + "code_disarm_required": "\u0412\u0438\u043c\u0430\u0433\u0430\u0442\u0438 PIN-\u043a\u043e\u0434 \u0434\u043b\u044f \u0437\u043d\u044f\u0442\u0442\u044f \u0437 \u043e\u0445\u043e\u0440\u043e\u043d\u0438", + "scan_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0438\u0442\u0443\u0432\u0430\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + }, + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438" + }, + "risco_to_ha": { + "data": { + "A": "\u0413\u0440\u0443\u043f\u0430 \u0410", + "B": "\u0413\u0440\u0443\u043f\u0430 B", + "C": "\u0413\u0440\u0443\u043f\u0430 C", + "D": "\u0413\u0440\u0443\u043f\u0430 D", + "arm": "\u041e\u0445\u043e\u0440\u043e\u043d\u0430 (AWAY)", + "partial_arm": "\u0427\u0430\u0441\u0442\u043a\u043e\u0432\u0430 \u043e\u0445\u043e\u0440\u043e\u043d\u0430 (STAY)" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0442\u0430\u043d \u0441\u0438\u0433\u043d\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u0457 Home Assistant \u043f\u0440\u0438 \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043d\u0456 \u0441\u0438\u0433\u043d\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u0457 Risco", + "title": "\u0417\u0456\u0441\u0442\u0430\u0432\u043b\u0435\u043d\u043d\u044f \u0441\u0442\u0430\u043d\u0456\u0432 Home Assistant \u0456 Risco" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rmvtransport/manifest.json b/homeassistant/components/rmvtransport/manifest.json index 693134dfeca..595e2d4834a 100644 --- a/homeassistant/components/rmvtransport/manifest.json +++ b/homeassistant/components/rmvtransport/manifest.json @@ -2,6 +2,10 @@ "domain": "rmvtransport", "name": "RMV", "documentation": "https://www.home-assistant.io/integrations/rmvtransport", - "requirements": ["PyRMVtransport==0.2.9"], - "codeowners": ["@cgtobi"] -} + "requirements": [ + "PyRMVtransport==0.2.10" + ], + "codeowners": [ + "@cgtobi" + ] +} \ No newline at end of file diff --git a/homeassistant/components/rmvtransport/sensor.py b/homeassistant/components/rmvtransport/sensor.py index 4a619437d52..eb053de8950 100644 --- a/homeassistant/components/rmvtransport/sensor.py +++ b/homeassistant/components/rmvtransport/sensor.py @@ -10,7 +10,6 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ATTR_ATTRIBUTION, CONF_NAME, TIME_MINUTES from homeassistant.exceptions import PlatformNotReady -from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle @@ -78,24 +77,20 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= """Set up the RMV departure sensor.""" timeout = config.get(CONF_TIMEOUT) - session = async_get_clientsession(hass) - - sensors = [] - for next_departure in config.get(CONF_NEXT_DEPARTURE): - sensors.append( - RMVDepartureSensor( - session, - next_departure[CONF_STATION], - next_departure.get(CONF_DESTINATIONS), - next_departure.get(CONF_DIRECTION), - next_departure.get(CONF_LINES), - next_departure.get(CONF_PRODUCTS), - next_departure.get(CONF_TIME_OFFSET), - next_departure.get(CONF_MAX_JOURNEYS), - next_departure.get(CONF_NAME), - timeout, - ) + sensors = [ + RMVDepartureSensor( + next_departure[CONF_STATION], + next_departure.get(CONF_DESTINATIONS), + next_departure.get(CONF_DIRECTION), + next_departure.get(CONF_LINES), + next_departure.get(CONF_PRODUCTS), + next_departure.get(CONF_TIME_OFFSET), + next_departure.get(CONF_MAX_JOURNEYS), + next_departure.get(CONF_NAME), + timeout, ) + for next_departure in config.get(CONF_NEXT_DEPARTURE) + ] tasks = [sensor.async_update() for sensor in sensors] if tasks: @@ -112,7 +107,6 @@ class RMVDepartureSensor(Entity): def __init__( self, - session, station, destinations, direction, @@ -128,7 +122,6 @@ class RMVDepartureSensor(Entity): self._name = name self._state = None self.data = RMVDepartureData( - session, station, destinations, direction, @@ -204,7 +197,6 @@ class RMVDepartureData: def __init__( self, - session, station_id, destinations, direction, @@ -223,7 +215,7 @@ class RMVDepartureData: self._products = products self._time_offset = time_offset self._max_journeys = max_journeys - self.rmv = RMVtransport(session, timeout) + self.rmv = RMVtransport(timeout) self.departures = [] self._error_notification = False @@ -263,10 +255,10 @@ class RMVDepartureData: if not dest_found: continue - if self._lines and journey["number"] not in self._lines: + elif self._lines and journey["number"] not in self._lines: continue - if journey["minutes"] < self._time_offset: + elif journey["minutes"] < self._time_offset: continue for attr in ["direction", "departure_time", "product", "minutes"]: diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 682576b534a..f1509edb6fb 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -8,7 +8,8 @@ "3810X", "4660X", "7820X", - "C105X" + "C105X", + "C135X" ] }, "ssdp": [ diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index e1662972cf6..e50c28d0a43 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -34,6 +34,7 @@ from homeassistant.const import ( STATE_STANDBY, ) from homeassistant.helpers import entity_platform +from homeassistant.helpers.network import is_internal_request from . import RokuDataUpdateCoordinator, RokuEntity, roku_exception_handler from .browse_media import build_item_response, library_payload @@ -250,9 +251,19 @@ class RokuMediaPlayer(RokuEntity, MediaPlayerEntity): async def async_browse_media(self, media_content_type=None, media_content_id=None): """Implement the websocket media browsing helper.""" + is_internal = is_internal_request(self.hass) - def _get_thumbnail_url(*args, **kwargs): - return self.get_browse_image_url(*args, **kwargs) + def _get_thumbnail_url( + media_content_type, media_content_id, media_image_id=None + ): + if is_internal: + if media_content_type == MEDIA_TYPE_APP and media_content_id: + return self.coordinator.roku.app_icon_url(media_content_id) + return None + + return self.get_browse_image_url( + media_content_type, media_content_id, media_image_id + ) if media_content_type in [None, "library"]: return library_payload(self.coordinator, _get_thumbnail_url) diff --git a/homeassistant/components/roku/translations/ca.json b/homeassistant/components/roku/translations/ca.json index e9ab61575b5..eb0564b5bde 100644 --- a/homeassistant/components/roku/translations/ca.json +++ b/homeassistant/components/roku/translations/ca.json @@ -9,6 +9,10 @@ }, "flow_title": "Roku: {name}", "step": { + "discovery_confirm": { + "description": "Vols configurar {name}?", + "title": "Roku" + }, "ssdp_confirm": { "description": "Vols configurar {name}?", "title": "Roku" diff --git a/homeassistant/components/roku/translations/cs.json b/homeassistant/components/roku/translations/cs.json index 7a83973a6f7..89ca523af47 100644 --- a/homeassistant/components/roku/translations/cs.json +++ b/homeassistant/components/roku/translations/cs.json @@ -9,6 +9,10 @@ }, "flow_title": "Roku: {name}", "step": { + "discovery_confirm": { + "description": "Chcete nastavit {name}?", + "title": "Roku" + }, "ssdp_confirm": { "description": "Chcete nastavit {name}?", "title": "Roku" diff --git a/homeassistant/components/roku/translations/de.json b/homeassistant/components/roku/translations/de.json index 9899aeba427..4bfb3c7503d 100644 --- a/homeassistant/components/roku/translations/de.json +++ b/homeassistant/components/roku/translations/de.json @@ -9,6 +9,10 @@ }, "flow_title": "Roku: {name}", "step": { + "discovery_confirm": { + "description": "M\u00f6chtest du {name} einrichten?", + "title": "Roku" + }, "ssdp_confirm": { "data": { "one": "eins", diff --git a/homeassistant/components/roku/translations/en.json b/homeassistant/components/roku/translations/en.json index 6facd1f3a7c..08db89f3677 100644 --- a/homeassistant/components/roku/translations/en.json +++ b/homeassistant/components/roku/translations/en.json @@ -9,6 +9,10 @@ }, "flow_title": "Roku: {name}", "step": { + "discovery_confirm": { + "description": "Do you want to set up {name}?", + "title": "Roku" + }, "ssdp_confirm": { "description": "Do you want to set up {name}?", "title": "Roku" diff --git a/homeassistant/components/roku/translations/es.json b/homeassistant/components/roku/translations/es.json index 78fb2580927..95e42643379 100644 --- a/homeassistant/components/roku/translations/es.json +++ b/homeassistant/components/roku/translations/es.json @@ -9,6 +9,10 @@ }, "flow_title": "Roku: {name}", "step": { + "discovery_confirm": { + "description": "\u00bfQuieres configurar {name} ?", + "title": "Roku" + }, "ssdp_confirm": { "description": "\u00bfQuieres configurar {name}?", "title": "Roku" diff --git a/homeassistant/components/roku/translations/et.json b/homeassistant/components/roku/translations/et.json index e4869d044c8..6727f539f57 100644 --- a/homeassistant/components/roku/translations/et.json +++ b/homeassistant/components/roku/translations/et.json @@ -9,6 +9,10 @@ }, "flow_title": "", "step": { + "discovery_confirm": { + "description": "Kas soovid seadistada {name}?", + "title": "" + }, "ssdp_confirm": { "description": "Kas soovid seadistada {name}?", "title": "" diff --git a/homeassistant/components/roku/translations/it.json b/homeassistant/components/roku/translations/it.json index 007be91d155..100d9992472 100644 --- a/homeassistant/components/roku/translations/it.json +++ b/homeassistant/components/roku/translations/it.json @@ -9,6 +9,10 @@ }, "flow_title": "Roku: {name}", "step": { + "discovery_confirm": { + "description": "Vuoi configurare {name}?", + "title": "Roku" + }, "ssdp_confirm": { "description": "Vuoi impostare {name}?", "title": "Roku" diff --git a/homeassistant/components/roku/translations/lb.json b/homeassistant/components/roku/translations/lb.json index 3aa8e5fa642..04ad814c6b4 100644 --- a/homeassistant/components/roku/translations/lb.json +++ b/homeassistant/components/roku/translations/lb.json @@ -9,6 +9,10 @@ }, "flow_title": "Roku: {name}", "step": { + "discovery_confirm": { + "description": "Soll {name} konfigur\u00e9iert ginn?", + "title": "Roku" + }, "ssdp_confirm": { "description": "Soll {name} konfigur\u00e9iert ginn?", "title": "Roku" diff --git a/homeassistant/components/roku/translations/no.json b/homeassistant/components/roku/translations/no.json index 029220b5859..dd4ce418141 100644 --- a/homeassistant/components/roku/translations/no.json +++ b/homeassistant/components/roku/translations/no.json @@ -9,6 +9,10 @@ }, "flow_title": "", "step": { + "discovery_confirm": { + "description": "Vil du konfigurere {name}?", + "title": "" + }, "ssdp_confirm": { "description": "Vil du sette opp {name} ?", "title": "" diff --git a/homeassistant/components/roku/translations/pl.json b/homeassistant/components/roku/translations/pl.json index 3231d6c4bb7..1d193acc0ff 100644 --- a/homeassistant/components/roku/translations/pl.json +++ b/homeassistant/components/roku/translations/pl.json @@ -9,6 +9,16 @@ }, "flow_title": "Roku: {name}", "step": { + "discovery_confirm": { + "data": { + "few": "kilka", + "many": "wiele", + "one": "jeden", + "other": "inne" + }, + "description": "Czy chcesz skonfigurowa\u0107 {name}?", + "title": "Roku" + }, "ssdp_confirm": { "data": { "few": "kilka", diff --git a/homeassistant/components/roku/translations/ru.json b/homeassistant/components/roku/translations/ru.json index b5dcddbe555..f7f36f41b27 100644 --- a/homeassistant/components/roku/translations/ru.json +++ b/homeassistant/components/roku/translations/ru.json @@ -9,6 +9,10 @@ }, "flow_title": "Roku: {name}", "step": { + "discovery_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?", + "title": "Roku" + }, "ssdp_confirm": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?", "title": "Roku" diff --git a/homeassistant/components/roku/translations/tr.json b/homeassistant/components/roku/translations/tr.json new file mode 100644 index 00000000000..0dca1a028b2 --- /dev/null +++ b/homeassistant/components/roku/translations/tr.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "unknown": "Beklenmeyen hata" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "discovery_confirm": { + "description": "{name} kurmak istiyor musunuz?", + "title": "Roku" + }, + "ssdp_confirm": { + "description": "{name} kurmak istiyor musunuz?" + }, + "user": { + "data": { + "host": "Ana Bilgisayar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/uk.json b/homeassistant/components/roku/translations/uk.json new file mode 100644 index 00000000000..b7db8875f8e --- /dev/null +++ b/homeassistant/components/roku/translations/uk.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "flow_title": "Roku: {name}", + "step": { + "discovery_confirm": { + "description": "\u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name}?", + "title": "Roku" + }, + "ssdp_confirm": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name}?", + "title": "Roku" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e Roku." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/zh-Hant.json b/homeassistant/components/roku/translations/zh-Hant.json index cfa3a4aa3b4..4b0566d66b0 100644 --- a/homeassistant/components/roku/translations/zh-Hant.json +++ b/homeassistant/components/roku/translations/zh-Hant.json @@ -9,6 +9,10 @@ }, "flow_title": "Roku\uff1a{name}", "step": { + "discovery_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f", + "title": "Roku" + }, "ssdp_confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f", "title": "Roku" diff --git a/homeassistant/components/roomba/__init__.py b/homeassistant/components/roomba/__init__.py index be85ec3619f..63deead7307 100644 --- a/homeassistant/components/roomba/__init__.py +++ b/homeassistant/components/roomba/__init__.py @@ -4,24 +4,17 @@ import logging import async_timeout from roombapy import Roomba, RoombaConnectionError -import voluptuous as vol -from homeassistant import config_entries, exceptions +from homeassistant import exceptions from homeassistant.const import CONF_HOST, CONF_PASSWORD -from homeassistant.core import callback -from homeassistant.helpers import config_validation as cv from .const import ( BLID, COMPONENTS, CONF_BLID, - CONF_CERT, CONF_CONTINUOUS, CONF_DELAY, CONF_NAME, - DEFAULT_CERT, - DEFAULT_CONTINUOUS, - DEFAULT_DELAY, DOMAIN, ROOMBA_SESSION, ) @@ -29,54 +22,9 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -def _has_all_unique_bilds(value): - """Validate that each vacuum configured has a unique bild. - - Uniqueness is determined case-independently. - """ - bilds = [device[CONF_BLID] for device in value] - schema = vol.Schema(vol.Unique()) - schema(bilds) - return value - - -DEVICE_SCHEMA = vol.All( - cv.deprecated(CONF_CERT), - vol.Schema( - { - vol.Required(CONF_HOST): str, - vol.Required(CONF_BLID): str, - vol.Required(CONF_PASSWORD): str, - vol.Optional(CONF_CERT, default=DEFAULT_CERT): str, - vol.Optional(CONF_CONTINUOUS, default=DEFAULT_CONTINUOUS): bool, - vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): int, - }, - ), -) - - -CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.All(cv.ensure_list, [DEVICE_SCHEMA], _has_all_unique_bilds)}, - extra=vol.ALLOW_EXTRA, -) - - async def async_setup(hass, config): """Set up the roomba environment.""" hass.data.setdefault(DOMAIN, {}) - - if DOMAIN not in config: - return True - for index, conf in enumerate(config[DOMAIN]): - _LOGGER.debug("Importing Roomba #%d - %s", index, conf[CONF_HOST]) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=conf, - ) - ) - return True @@ -88,8 +36,8 @@ async def async_setup_entry(hass, config_entry): hass.config_entries.async_update_entry( config_entry, options={ - "continuous": config_entry.data[CONF_CONTINUOUS], - "delay": config_entry.data[CONF_DELAY], + CONF_CONTINUOUS: config_entry.data[CONF_CONTINUOUS], + CONF_DELAY: config_entry.data[CONF_DELAY], }, ) @@ -184,12 +132,5 @@ def roomba_reported_state(roomba): return roomba.master_state.get("state", {}).get("reported", {}) -@callback -def _async_find_matching_config_entry(hass, prefix): - for entry in hass.config_entries.async_entries(DOMAIN): - if entry.unique_id == prefix: - return entry - - class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py index 166b5992d86..4b0b76b44c9 100644 --- a/homeassistant/components/roomba/config_flow.py +++ b/homeassistant/components/roomba/config_flow.py @@ -1,8 +1,14 @@ """Config flow to configure roomba component.""" + +import asyncio + from roombapy import Roomba +from roombapy.discovery import RoombaDiscovery +from roombapy.getpassword import RoombaPassword import voluptuous as vol from homeassistant import config_entries, core +from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS from homeassistant.const import CONF_HOST, CONF_PASSWORD from homeassistant.core import callback @@ -18,14 +24,15 @@ from .const import ( ) from .const import DOMAIN # pylint:disable=unused-import -DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_HOST): str, - vol.Required(CONF_BLID): str, - vol.Required(CONF_PASSWORD): str, - vol.Optional(CONF_CONTINUOUS, default=DEFAULT_CONTINUOUS): bool, - vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): int, - } +ROOMBA_DISCOVERY_LOCK = "roomba_discovery_lock" + +DEFAULT_OPTIONS = {CONF_CONTINUOUS: DEFAULT_CONTINUOUS, CONF_DELAY: DEFAULT_DELAY} + +MAX_NUM_DEVICES_TO_DISCOVER = 25 + +AUTH_HELP_URL_KEY = "auth_help_url" +AUTH_HELP_URL_VALUE = ( + "https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials" ) @@ -57,36 +64,206 @@ class RoombaConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + def __init__(self): + """Initialize the roomba flow.""" + self.discovered_robots = {} + self.name = None + self.blid = None + self.host = None + @staticmethod @callback def async_get_options_flow(config_entry): """Get the options flow for this handler.""" return OptionsFlowHandler(config_entry) - async def async_step_import(self, import_info): - """Set the config entry up from yaml.""" - return await self.async_step_user(import_info) + async def async_step_dhcp(self, dhcp_discovery): + """Handle dhcp discovery.""" + if self._async_host_already_configured(dhcp_discovery[IP_ADDRESS]): + return self.async_abort(reason="already_configured") + + if not dhcp_discovery[HOSTNAME].startswith("iRobot-"): + return self.async_abort(reason="not_irobot_device") + + blid = _async_blid_from_hostname(dhcp_discovery[HOSTNAME]) + await self.async_set_unique_id(blid) + self._abort_if_unique_id_configured( + updates={CONF_HOST: dhcp_discovery[IP_ADDRESS]} + ) + + self.host = dhcp_discovery[IP_ADDRESS] + self.blid = blid + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context["title_placeholders"] = {"host": self.host, "name": self.blid} + return await self.async_step_user() + + async def _async_start_link(self): + """Start linking.""" + device = self.discovered_robots[self.host] + self.blid = device.blid + self.name = device.robot_name + await self.async_set_unique_id(self.blid, raise_on_progress=False) + self._abort_if_unique_id_configured() + return await self.async_step_link() async def async_step_user(self, user_input=None): - """Handle a flow initialized by the user.""" + """Handle a flow start.""" + # Check if user chooses manual entry + if user_input is not None and not user_input.get(CONF_HOST): + return await self.async_step_manual() + + if ( + user_input is not None + and self.discovered_robots is not None + and user_input[CONF_HOST] in self.discovered_robots + ): + self.host = user_input[CONF_HOST] + return await self._async_start_link() + + already_configured = self._async_current_ids(False) + discovery = _async_get_roomba_discovery() + + async with self.hass.data.setdefault(ROOMBA_DISCOVERY_LOCK, asyncio.Lock()): + devices = await self.hass.async_add_executor_job(discovery.get_all) + + if devices: + # Find already configured hosts + self.discovered_robots = { + device.ip: device + for device in devices + if device.blid not in already_configured + } + if self.host and self.host in self.discovered_robots: + # From discovery + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context["title_placeholders"] = { + "host": self.host, + "name": self.discovered_robots[self.host].robot_name, + } + return await self._async_start_link() + + if not self.discovered_robots: + return await self.async_step_manual() + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Optional("host"): vol.In( + { + **{ + device.ip: f"{device.robot_name} ({device.ip})" + for device in devices + if device.blid not in already_configured + }, + None: "Manually add a Roomba or Braava", + } + ) + } + ), + ) + + async def async_step_manual(self, user_input=None): + """Handle manual device setup.""" + if user_input is None: + return self.async_show_form( + step_id="manual", + description_placeholders={AUTH_HELP_URL_KEY: AUTH_HELP_URL_VALUE}, + data_schema=vol.Schema( + { + vol.Required(CONF_HOST, default=self.host): str, + vol.Required(CONF_BLID, default=self.blid): str, + } + ), + ) + + if any( + user_input["host"] == entry.data.get("host") + for entry in self._async_current_entries() + ): + return self.async_abort(reason="already_configured") + + self.host = user_input[CONF_HOST] + self.blid = user_input[CONF_BLID] + await self.async_set_unique_id(self.blid, raise_on_progress=False) + self._abort_if_unique_id_configured() + return await self.async_step_link() + + async def async_step_link(self, user_input=None): + """Attempt to link with the Roomba. + + Given a configured host, will ask the user to press the home and target buttons + to connect to the device. + """ + if user_input is None: + return self.async_show_form( + step_id="link", + description_placeholders={CONF_NAME: self.name or self.blid}, + ) + + try: + password = await self.hass.async_add_executor_job( + RoombaPassword(self.host).get_password + ) + except ConnectionRefusedError: + return await self.async_step_link_manual() + + if not password: + return await self.async_step_link_manual() + + config = { + CONF_HOST: self.host, + CONF_BLID: self.blid, + CONF_PASSWORD: password, + **DEFAULT_OPTIONS, + } + + if not self.name: + try: + info = await validate_input(self.hass, config) + except CannotConnect: + return self.async_abort(reason="cannot_connect") + + await async_disconnect_or_timeout(self.hass, info[ROOMBA_SESSION]) + self.name = info[CONF_NAME] + + return self.async_create_entry(title=self.name, data=config) + + async def async_step_link_manual(self, user_input=None): + """Handle manual linking.""" errors = {} if user_input is not None: - await self.async_set_unique_id(user_input[CONF_BLID]) - self._abort_if_unique_id_configured() + config = { + CONF_HOST: self.host, + CONF_BLID: self.blid, + CONF_PASSWORD: user_input[CONF_PASSWORD], + **DEFAULT_OPTIONS, + } try: - info = await validate_input(self.hass, user_input) + info = await validate_input(self.hass, config) except CannotConnect: errors = {"base": "cannot_connect"} - if "base" not in errors: + if not errors: await async_disconnect_or_timeout(self.hass, info[ROOMBA_SESSION]) - return self.async_create_entry(title=info[CONF_NAME], data=user_input) + return self.async_create_entry(title=info[CONF_NAME], data=config) return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors + step_id="link_manual", + description_placeholders={AUTH_HELP_URL_KEY: AUTH_HELP_URL_VALUE}, + data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}), + errors=errors, ) + @callback + def _async_host_already_configured(self, host): + """See if we already have an entry matching the host.""" + for entry in self._async_current_entries(): + if entry.data.get(CONF_HOST) == host: + return True + return False + class OptionsFlowHandler(config_entries.OptionsFlow): """Handle options.""" @@ -119,3 +296,17 @@ class OptionsFlowHandler(config_entries.OptionsFlow): } ), ) + + +@callback +def _async_get_roomba_discovery(): + """Create a discovery object.""" + discovery = RoombaDiscovery() + discovery.amount_of_broadcasted_messages = MAX_NUM_DEVICES_TO_DISCOVER + return discovery + + +@callback +def _async_blid_from_hostname(hostname): + """Extract the blid from the hostname.""" + return hostname.split("-")[1].split(".")[0] diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index 808c7eb9432..5ceb44ff780 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -1,8 +1,9 @@ { "domain": "roomba", - "name": "iRobot Roomba", + "name": "iRobot Roomba and Braava", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roomba", "requirements": ["roombapy==1.6.2"], - "codeowners": ["@pschmitt", "@cyr-ius", "@shenxn"] + "codeowners": ["@pschmitt", "@cyr-ius", "@shenxn"], + "dhcp": [{"hostname":"irobot-*","macaddress":"501479*"}] } diff --git a/homeassistant/components/roomba/strings.json b/homeassistant/components/roomba/strings.json index cbe7c06ae36..512e27a758e 100644 --- a/homeassistant/components/roomba/strings.json +++ b/homeassistant/components/roomba/strings.json @@ -1,21 +1,42 @@ { "config": { + "flow_title": "iRobot {name} ({host})", "step": { - "user": { - "title": "Connect to the device", - "description": "Currently retrieving the BLID and password is a manual process. Please follow the steps outlined in the documentation at: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", + "init": { + "title": "Automaticlly connect to the device", + "description": "Select a Roomba or Braava.", + "data": { + "host": "[%key:common::config_flow::data::host%]" + } + }, + "manual": { + "title": "Manually connect to the device", + "description": "No Roomba or Braava have been discovered on your network. The BLID is the portion of the device hostname after `iRobot-`. Please follow the steps outlined in the documentation at: {auth_help_url}", "data": { "host": "[%key:common::config_flow::data::host%]", - "blid": "BLID", - "password": "[%key:common::config_flow::data::password%]", - "continuous": "Continuous", - "delay": "Delay" + "blid": "BLID" } - } + }, + "link": { + "title": "Retrieve Password", + "description": "Press and hold the Home button on {name} until the device generates a sound (about two seconds)." + }, + "link_manual": { + "title": "Enter Password", + "description": "The password could not be retrivied from the device automatically. Please follow the steps outlined in the documentation at: {auth_help_url}", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } + } }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" - } + }, + "abort": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "not_irobot_device": "Discovered device is not an iRobot device" + } }, "options": { "step": { diff --git a/homeassistant/components/roomba/translations/ca.json b/homeassistant/components/roomba/translations/ca.json index af358678144..b2fe68c876c 100644 --- a/homeassistant/components/roomba/translations/ca.json +++ b/homeassistant/components/roomba/translations/ca.json @@ -1,9 +1,41 @@ { "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat", + "cannot_connect": "Ha fallat la connexi\u00f3", + "not_irobot_device": "El dispositiu descobert no \u00e9s un dispositiu iRobot" + }, "error": { "cannot_connect": "Ha fallat la connexi\u00f3" }, + "flow_title": "iRobot {name} ({host})", "step": { + "init": { + "data": { + "host": "Amfitri\u00f3" + }, + "description": "Selecciona un/a Roomba o Braava.", + "title": "Connecta't al dispositiu autom\u00e0ticament" + }, + "link": { + "description": "Mant\u00e9 premut el bot\u00f3 d'inici a {name} fins que el dispositiu emeti un so (aproximadament dos segons).", + "title": "Recupera la contrasenya" + }, + "link_manual": { + "data": { + "password": "Contrasenya" + }, + "description": "No s'ha pogut obtenir la contrasenya del dispositiu autom\u00e0ticament. Segueix els passos de la seg\u00fcent documentaci\u00f3: {auth_help_url}", + "title": "Introdueix contrasenya" + }, + "manual": { + "data": { + "blid": "BLID", + "host": "Amfitri\u00f3" + }, + "description": "No s'ha descobert cap Roomba ni cap Braava a la teva xarxa. El BLID \u00e9s la part del nom d'amfitri\u00f3 del dispositiu despr\u00e9s de `iRobot-`. Segueix els passos de la seg\u00fcent documentaci\u00f3: {auth_help_url}", + "title": "Connecta't al dispositiu manualment" + }, "user": { "data": { "blid": "BLID", diff --git a/homeassistant/components/roomba/translations/cs.json b/homeassistant/components/roomba/translations/cs.json index fdf4aff22c9..d94d39f8136 100644 --- a/homeassistant/components/roomba/translations/cs.json +++ b/homeassistant/components/roomba/translations/cs.json @@ -1,9 +1,29 @@ { "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" }, + "flow_title": "iRobot {name} ({host})", "step": { + "init": { + "data": { + "host": "Hostitel" + } + }, + "link_manual": { + "data": { + "password": "Heslo" + } + }, + "manual": { + "data": { + "host": "Hostitel" + } + }, "user": { "data": { "delay": "Zpo\u017ed\u011bn\u00ed", diff --git a/homeassistant/components/roomba/translations/de.json b/homeassistant/components/roomba/translations/de.json index 2f6ef37d13c..780d406bcaf 100644 --- a/homeassistant/components/roomba/translations/de.json +++ b/homeassistant/components/roomba/translations/de.json @@ -1,9 +1,40 @@ { "config": { - "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut" + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", + "not_irobot_device": "Das erkannte Ger\u00e4t ist kein iRobot-Ger\u00e4t" }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "flow_title": "iRobot {name} ({host})", "step": { + "init": { + "data": { + "host": "Host" + }, + "description": "W\u00e4hle einen Roomba oder Braava aus.", + "title": "Automatisch mit dem Ger\u00e4t verbinden" + }, + "link": { + "description": "Halte die Home-Taste von {name} gedr\u00fcckt, bis das Ger\u00e4t einen Ton erzeugt (ca. zwei Sekunden).", + "title": "Passwort abrufen" + }, + "link_manual": { + "data": { + "password": "Passwort" + }, + "description": "Das Passwort konnte nicht automatisch vom Ger\u00e4t abgerufen werden. Bitte die in der Dokumentation beschriebenen Schritte unter {auth_help_url} befolgen", + "title": "Passwort eingeben" + }, + "manual": { + "data": { + "blid": "BLID", + "host": "Host" + }, + "title": "Manuell mit dem Ger\u00e4t verbinden" + }, "user": { "data": { "blid": "BLID", diff --git a/homeassistant/components/roomba/translations/en.json b/homeassistant/components/roomba/translations/en.json index b6222f2adf8..8d449e18815 100644 --- a/homeassistant/components/roomba/translations/en.json +++ b/homeassistant/components/roomba/translations/en.json @@ -1,9 +1,41 @@ { "config": { + "abort": { + "already_configured": "Device is already configured", + "cannot_connect": "Failed to connect", + "not_irobot_device": "Discovered device is not an iRobot device" + }, "error": { "cannot_connect": "Failed to connect" }, + "flow_title": "iRobot {name} ({host})", "step": { + "init": { + "data": { + "host": "Host" + }, + "description": "Select a Roomba or Braava.", + "title": "Automaticlly connect to the device" + }, + "link": { + "description": "Press and hold the Home button on {name} until the device generates a sound (about two seconds).", + "title": "Retrieve Password" + }, + "link_manual": { + "data": { + "password": "Password" + }, + "description": "The password could not be retrivied from the device automatically. Please follow the steps outlined in the documentation at: {auth_help_url}", + "title": "Enter Password" + }, + "manual": { + "data": { + "blid": "BLID", + "host": "Host" + }, + "description": "No Roomba or Braava have been discovered on your network. The BLID is the portion of the device hostname after `iRobot-`. Please follow the steps outlined in the documentation at: {auth_help_url}", + "title": "Manually connect to the device" + }, "user": { "data": { "blid": "BLID", diff --git a/homeassistant/components/roomba/translations/es.json b/homeassistant/components/roomba/translations/es.json index a49022c2d3d..29f0b47a655 100644 --- a/homeassistant/components/roomba/translations/es.json +++ b/homeassistant/components/roomba/translations/es.json @@ -1,9 +1,41 @@ { "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar", + "not_irobot_device": "El dispositivo descubierto no es un dispositivo iRobot" + }, "error": { "cannot_connect": "No se pudo conectar" }, + "flow_title": "iRobot {name} ({host})", "step": { + "init": { + "data": { + "host": "Host" + }, + "description": "Selecciona una Roomba o Braava.", + "title": "Conectar autom\u00e1ticamente con el dispositivo" + }, + "link": { + "description": "Mant\u00e9n pulsado el bot\u00f3n Inicio en {name} hasta que el dispositivo genere un sonido (aproximadamente dos segundos).", + "title": "Recuperar la contrase\u00f1a" + }, + "link_manual": { + "data": { + "password": "Contrase\u00f1a" + }, + "description": "No se pudo recuperar la contrase\u00f1a desde el dispositivo de forma autom\u00e1tica. Por favor, sigue los pasos descritos en la documentaci\u00f3n en: {auth_help_url}", + "title": "Escribe la contrase\u00f1a" + }, + "manual": { + "data": { + "blid": "BLID", + "host": "Host" + }, + "description": "No se ha descubierto ning\u00fan dispositivo Roomba ni Braava en tu red. El BLID es la parte del nombre de host del dispositivo despu\u00e9s de 'iRobot-'. Por favor, sigue los pasos descritos en la documentaci\u00f3n en: {auth_help_url}", + "title": "Conectar manualmente con el dispositivo" + }, "user": { "data": { "blid": "BLID", diff --git a/homeassistant/components/roomba/translations/et.json b/homeassistant/components/roomba/translations/et.json index 92da58fa146..e038257c12d 100644 --- a/homeassistant/components/roomba/translations/et.json +++ b/homeassistant/components/roomba/translations/et.json @@ -1,9 +1,41 @@ { "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "cannot_connect": "\u00dchendamine nurjus", + "not_irobot_device": "Leitud seade ei ole iRoboti seade" + }, "error": { "cannot_connect": "\u00dchendamine nurjus" }, + "flow_title": "iRobot {name} ( {host} )", "step": { + "init": { + "data": { + "host": "Host" + }, + "description": "Vali Roomba v\u00f5i Braava seade.", + "title": "\u00dchendu seadmega automaatselt" + }, + "link": { + "description": "Vajuta ja hoia all seadme {name} nuppu Home kuni seade teeb piiksu (umbes kaks sekundit).", + "title": "Hangi salas\u00f5na" + }, + "link_manual": { + "data": { + "password": "Salas\u00f5na" + }, + "description": "Salas\u00f5na ei \u00f5nnestunud seadmest automaatselt hankida. J\u00e4rgi dokumentatsioonis toodud juhiseid: {auth_help_url}", + "title": "Sisesta salas\u00f5na" + }, + "manual": { + "data": { + "blid": "", + "host": "Host" + }, + "description": "V\u00f5rgus ei tuvastatud \u00fchtegi Roomba ega Braava seadet. BLID on seadme hostinime osa p\u00e4rast iRobot-`. J\u00e4rgi dokumentatsioonis toodud juhiseid: {auth_help_url}", + "title": "\u00dchenda seadmega k\u00e4sitsi" + }, "user": { "data": { "blid": "", diff --git a/homeassistant/components/roomba/translations/fr.json b/homeassistant/components/roomba/translations/fr.json index 1ec97dd3842..8142d3acf13 100644 --- a/homeassistant/components/roomba/translations/fr.json +++ b/homeassistant/components/roomba/translations/fr.json @@ -1,9 +1,38 @@ { "config": { + "abort": { + "cannot_connect": "Echec de connection" + }, "error": { "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer" }, "step": { + "init": { + "data": { + "host": "H\u00f4te" + }, + "description": "S\u00e9lectionnez un Roomba ou un Braava.", + "title": "Se connecter automatiquement \u00e0 l'appareil" + }, + "link": { + "description": "Appuyez sur le bouton Accueil et maintenez-le enfonc\u00e9 jusqu'\u00e0 ce que l'appareil \u00e9mette un son (environ deux secondes).", + "title": "R\u00e9cup\u00e9rer le mot de passe" + }, + "link_manual": { + "data": { + "password": "Mot de passe" + }, + "description": "Le mot de passe n'a pas pu \u00eatre r\u00e9cup\u00e9r\u00e9 automatiquement \u00e0 partir de l'appareil. Veuillez suivre les \u00e9tapes d\u00e9crites dans la documentation \u00e0 {auth_help_url}", + "title": "Entrer le mot de passe" + }, + "manual": { + "data": { + "blid": "BLID", + "host": "H\u00f4te" + }, + "description": "Aucun Roomba ou Braava d\u00e9couvert sur votre r\u00e9seau. Le BLID est la partie du nom d'h\u00f4te du p\u00e9riph\u00e9rique apr\u00e8s `iRobot-`. Veuillez suivre les \u00e9tapes d\u00e9crites dans la documentation \u00e0 {auth_help_url}", + "title": "Se connecter manuellement \u00e0 l'appareil" + }, "user": { "data": { "blid": "BLID", diff --git a/homeassistant/components/roomba/translations/it.json b/homeassistant/components/roomba/translations/it.json index d109aa8bcc0..b9e01faf16c 100644 --- a/homeassistant/components/roomba/translations/it.json +++ b/homeassistant/components/roomba/translations/it.json @@ -1,9 +1,41 @@ { "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "cannot_connect": "Impossibile connettersi", + "not_irobot_device": "Il dispositivo rilevato non \u00e8 un dispositivo iRobot" + }, "error": { "cannot_connect": "Impossibile connettersi" }, + "flow_title": "iRobot {name} ({host})", "step": { + "init": { + "data": { + "host": "Host" + }, + "description": "Seleziona un Roomba o un Braava.", + "title": "Connettiti automaticamente al dispositivo" + }, + "link": { + "description": "Tieni premuto il pulsante Home su {name} fino a quando il dispositivo non genera un suono (circa due secondi).", + "title": "Recupera password" + }, + "link_manual": { + "data": { + "password": "Password" + }, + "description": "La password non pu\u00f2 essere recuperata automaticamente dal dispositivo. Segui le istruzioni indicate sulla documentazione a: {auth_help_url}", + "title": "Inserisci la password" + }, + "manual": { + "data": { + "blid": "BLID", + "host": "Host" + }, + "description": "Non sono stati trovati Roomba o Braava all'interno della tua rete. Il BLID \u00e8 la porzione del nome host del dispositivo dopo `iRobot-`. Segui le istruzioni indicate sulla documentazione a: {auth_help_url}", + "title": "Connettiti manualmente al dispositivo" + }, "user": { "data": { "blid": "BLID", diff --git a/homeassistant/components/roomba/translations/lb.json b/homeassistant/components/roomba/translations/lb.json index d3fc631f5df..500aa4fbee6 100644 --- a/homeassistant/components/roomba/translations/lb.json +++ b/homeassistant/components/roomba/translations/lb.json @@ -1,9 +1,35 @@ { "config": { + "abort": { + "cannot_connect": "Feeler beim verbannen" + }, "error": { "cannot_connect": "Feeler beim verbannen" }, "step": { + "init": { + "data": { + "host": "Host" + }, + "description": "Ee Roomba oder Bravaa auswielen.", + "title": "Automatesch mam Apparat verbannen" + }, + "link": { + "title": "Passwuert ausliesen" + }, + "link_manual": { + "data": { + "password": "Passwuert" + }, + "title": "Passwuert aginn" + }, + "manual": { + "data": { + "blid": "BLID", + "host": "Host" + }, + "title": "Manuell mam Apparat verbannen" + }, "user": { "data": { "blid": "BLID", diff --git a/homeassistant/components/roomba/translations/no.json b/homeassistant/components/roomba/translations/no.json index adf13cb57af..2bfe9f774d1 100644 --- a/homeassistant/components/roomba/translations/no.json +++ b/homeassistant/components/roomba/translations/no.json @@ -1,9 +1,41 @@ { "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert", + "cannot_connect": "Tilkobling mislyktes", + "not_irobot_device": "Oppdaget enhet er ikke en iRobot-enhet" + }, "error": { "cannot_connect": "Tilkobling mislyktes" }, + "flow_title": "", "step": { + "init": { + "data": { + "host": "Vert" + }, + "description": "Velg en Roomba eller Braava", + "title": "Koble automatisk til enheten" + }, + "link": { + "description": "Trykk og hold inne Hjem-knappen p\u00e5 {name} til enheten genererer en lyd (omtrent to sekunder)", + "title": "Hent passord" + }, + "link_manual": { + "data": { + "password": "Passord" + }, + "description": "Passordet kunne ikke hentes automatisk fra enheten. F\u00f8lg trinnene som er beskrevet i dokumentasjonen p\u00e5: {auth_help_url}", + "title": "Skriv inn passord" + }, + "manual": { + "data": { + "blid": "", + "host": "Vert" + }, + "description": "Ingen Roomba eller Braava har blitt oppdaget i nettverket ditt. BLID er delen av enhetens vertsnavn etter `iRobot-`. F\u00f8lg trinnene som er beskrevet i dokumentasjonen p\u00e5: {auth_help_url}", + "title": "Koble til enheten manuelt" + }, "user": { "data": { "blid": "Blid", diff --git a/homeassistant/components/roomba/translations/pl.json b/homeassistant/components/roomba/translations/pl.json index b2a4ab89cbe..e4951a366dd 100644 --- a/homeassistant/components/roomba/translations/pl.json +++ b/homeassistant/components/roomba/translations/pl.json @@ -1,9 +1,41 @@ { "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "not_irobot_device": "Wykryte urz\u0105dzenie nie jest urz\u0105dzeniem iRobot" + }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, + "flow_title": "iRobot {name} ({host})", "step": { + "init": { + "data": { + "host": "Nazwa hosta lub adres IP" + }, + "description": "Wybierz Roomb\u0119 lub Braava", + "title": "Po\u0142\u0105cz si\u0119 automatycznie z urz\u0105dzeniem" + }, + "link": { + "description": "Naci\u015bnij i przytrzymaj przycisk Home na {name} a\u017c urz\u0105dzenie wygeneruje d\u017awi\u0119k (oko\u0142o dwie sekundy).", + "title": "Odzyskiwanie has\u0142a" + }, + "link_manual": { + "data": { + "password": "Has\u0142o" + }, + "description": "Nie mo\u017cna automatycznie pobra\u0107 has\u0142a z urz\u0105dzenia. Post\u0119puj zgodnie z instrukcjami podanymi w dokumentacji pod adresem: {auth_help_url}", + "title": "Wprowad\u017a has\u0142o" + }, + "manual": { + "data": { + "blid": "BLID", + "host": "Nazwa hosta lub adres IP" + }, + "description": "W Twojej sieci nie wykryto urz\u0105dzenia Roomba ani Braava. BLID to cz\u0119\u015b\u0107 nazwy hosta urz\u0105dzenia po `iRobot-`. Post\u0119puj zgodnie z instrukcjami podanymi w dokumentacji pod adresem: {auth_help_url}", + "title": "R\u0119czne po\u0142\u0105czenie z urz\u0105dzeniem" + }, "user": { "data": { "blid": "BLID", diff --git a/homeassistant/components/roomba/translations/pt.json b/homeassistant/components/roomba/translations/pt.json index 0156fd48a62..6036e870e6c 100644 --- a/homeassistant/components/roomba/translations/pt.json +++ b/homeassistant/components/roomba/translations/pt.json @@ -1,9 +1,16 @@ { "config": { + "abort": { + "not_irobot_device": "O dispositivo descoberto n\u00e3o \u00e9 um dispositivo iRobot" + }, "error": { "cannot_connect": "Falha ao conectar, tente novamente" }, + "flow_title": "iRobot {name} ({host})", "step": { + "link": { + "title": "Recuperar Palavra-passe" + }, "user": { "data": { "continuous": "Cont\u00ednuo", diff --git a/homeassistant/components/roomba/translations/ru.json b/homeassistant/components/roomba/translations/ru.json index ee1192f69ec..979bb9bc70f 100644 --- a/homeassistant/components/roomba/translations/ru.json +++ b/homeassistant/components/roomba/translations/ru.json @@ -1,9 +1,41 @@ { "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "not_irobot_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 iRobot." + }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, + "flow_title": "iRobot {name} ({host})", "step": { + "init": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u044b\u043b\u0435\u0441\u043e\u0441 \u0438\u0437 \u043c\u043e\u0434\u0435\u043b\u0435\u0439 Roomba \u0438\u043b\u0438 Braava.", + "title": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u043e\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + }, + "link": { + "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u0438 \u0443\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0439\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 Home \u043d\u0430 {name}, \u043f\u043e\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u0438\u0437\u0434\u0430\u0441\u0442 \u0437\u0432\u0443\u043a (\u043e\u043a\u043e\u043b\u043e \u0434\u0432\u0443\u0445 \u0441\u0435\u043a\u0443\u043d\u0434).", + "title": "\u041f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u0435 \u043f\u0430\u0440\u043e\u043b\u044f" + }, + "link_manual": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u0435\u0441\u043a\u0438 \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439: {auth_help_url}.", + "title": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043f\u0430\u0440\u043e\u043b\u044c" + }, + "manual": { + "data": { + "blid": "BLID", + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u0412 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u043e\u0439 \u0441\u0435\u0442\u0438 \u043d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043e \u043f\u044b\u043b\u0435\u0441\u043e\u0441\u043e\u0432 Roomba \u0438\u043b\u0438 Braava. BLID - \u044d\u0442\u043e \u0447\u0430\u0441\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0433\u043e \u0438\u043c\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u0441\u043b\u0435\u0434\u0443\u044e\u0449\u0430\u044f \u043f\u043e\u0441\u043b\u0435 \"iRobot-\". \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439: {auth_help_url}.", + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443 \u0432\u0440\u0443\u0447\u043d\u0443\u044e" + }, "user": { "data": { "blid": "BLID", diff --git a/homeassistant/components/roomba/translations/tr.json b/homeassistant/components/roomba/translations/tr.json new file mode 100644 index 00000000000..3d85144c188 --- /dev/null +++ b/homeassistant/components/roomba/translations/tr.json @@ -0,0 +1,60 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "not_irobot_device": "Bulunan cihaz bir iRobot cihaz\u0131 de\u011fil" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "flow_title": "iRobot {name} ( {host} )", + "step": { + "init": { + "data": { + "host": "Ana Bilgisayar" + }, + "description": "Roomba veya Braava'y\u0131 se\u00e7in.", + "title": "Cihaza otomatik olarak ba\u011flan" + }, + "link": { + "description": "Cihaz bir ses olu\u015fturana kadar (yakla\u015f\u0131k iki saniye) {name} \u00fczerindeki Ana Sayfa d\u00fc\u011fmesini bas\u0131l\u0131 tutun.", + "title": "\u015eifre Al" + }, + "link_manual": { + "data": { + "password": "\u015eifre" + }, + "description": "Parola ayg\u0131ttan otomatik olarak al\u0131namad\u0131. L\u00fctfen belgelerde belirtilen ad\u0131mlar\u0131 izleyin: {auth_help_url}", + "title": "\u015eifre Girin" + }, + "manual": { + "data": { + "blid": "BLID", + "host": "Ana Bilgisayar" + }, + "title": "Cihaza manuel olarak ba\u011flan\u0131n" + }, + "user": { + "data": { + "continuous": "S\u00fcrekli", + "delay": "Gecikme", + "host": "Ana Bilgisayar", + "password": "Parola" + }, + "description": "\u015eu anda BLID ve parola alma manuel bir i\u015flemdir. L\u00fctfen a\u015fa\u011f\u0131daki belgelerde belirtilen ad\u0131mlar\u0131 izleyin: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", + "title": "Cihaza ba\u011flan\u0131n" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "continuous": "S\u00fcrekli", + "delay": "Gecikme" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/uk.json b/homeassistant/components/roomba/translations/uk.json new file mode 100644 index 00000000000..833a35f62f3 --- /dev/null +++ b/homeassistant/components/roomba/translations/uk.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "blid": "BLID", + "continuous": "\u0411\u0435\u0437\u043f\u0435\u0440\u0435\u0440\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c", + "delay": "\u0417\u0430\u0442\u0440\u0438\u043c\u043a\u0430 (\u0441\u0435\u043a.)", + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438, \u0449\u043e\u0431 \u0434\u0456\u0437\u043d\u0430\u0442\u0438\u0441\u044f \u044f\u043a \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 BLID \u0456 \u043f\u0430\u0440\u043e\u043b\u044c:\nhttps://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "continuous": "\u0411\u0435\u0437\u043f\u0435\u0440\u0435\u0440\u0432\u043d\u043e", + "delay": "\u0417\u0430\u0442\u0440\u0438\u043c\u043a\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roomba/translations/zh-Hant.json b/homeassistant/components/roomba/translations/zh-Hant.json index 932e5cadd75..790eba79c03 100644 --- a/homeassistant/components/roomba/translations/zh-Hant.json +++ b/homeassistant/components/roomba/translations/zh-Hant.json @@ -1,9 +1,41 @@ { "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "not_irobot_device": "\u6240\u767c\u73fe\u7684\u88dd\u7f6e\u4e26\u975e iRobot \u88dd\u7f6e" + }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" }, + "flow_title": "iRobot {name} ({host})", "step": { + "init": { + "data": { + "host": "\u4e3b\u6a5f\u7aef" + }, + "description": "\u9078\u64c7 Roomba \u6216 Braava\u3002", + "title": "\u81ea\u52d5\u9023\u7dda\u81f3\u88dd\u7f6e" + }, + "link": { + "description": "\u8acb\u6309\u4f4f {name} \u4e0a\u7684 Home \u9375\u76f4\u5230\u88dd\u7f6e\u767c\u51fa\u8072\u97f3\uff08\u7d04\u5169\u79d2\uff09\u3002", + "title": "\u91cd\u7f6e\u5bc6\u78bc" + }, + "link_manual": { + "data": { + "password": "\u5bc6\u78bc" + }, + "description": "\u5bc6\u78bc\u53ef\u81ea\u52d5\u81ea\u88dd\u7f6e\u4e0a\u53d6\u5f97\u3002\u8acb\u53c3\u95b1\u4ee5\u4e0b\u6587\u4ef6\u7684\u6b65\u9a5f\u9032\u884c\u8a2d\u5b9a\uff1a{auth_help_url}", + "title": "\u8f38\u5165\u5bc6\u78bc" + }, + "manual": { + "data": { + "blid": "BLID", + "host": "\u4e3b\u6a5f\u7aef" + }, + "description": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230 Roomba \u6216 Braava\u3002BLID \u88dd\u7f6e\u65bc\u4e3b\u6a5f\u7aef\u7684\u90e8\u5206\u540d\u7a31\u70ba `iRobot-` \u958b\u982d\u3002\u8acb\u53c3\u95b1\u4ee5\u4e0b\u6587\u4ef6\u7684\u6b65\u9a5f\u9032\u884c\u8a2d\u5b9a\uff1a{auth_help_url}", + "title": "\u624b\u52d5\u9023\u7dda\u81f3\u88dd\u7f6e" + }, "user": { "data": { "blid": "BLID", diff --git a/homeassistant/components/roon/config_flow.py b/homeassistant/components/roon/config_flow.py index f8af2bac0e6..c6ed4436981 100644 --- a/homeassistant/components/roon/config_flow.py +++ b/homeassistant/components/roon/config_flow.py @@ -2,7 +2,7 @@ import asyncio import logging -from roonapi import RoonApi +from roonapi import RoonApi, RoonDiscovery import voluptuous as vol from homeassistant import config_entries, core, exceptions @@ -10,6 +10,7 @@ from homeassistant.const import CONF_API_KEY, CONF_HOST from .const import ( # pylint: disable=unused-import AUTHENTICATE_TIMEOUT, + CONF_ROON_ID, DEFAULT_NAME, DOMAIN, ROON_APPINFO, @@ -25,36 +26,79 @@ TIMEOUT = 120 class RoonHub: """Interact with roon during config flow.""" - def __init__(self, host): - """Initialize.""" - self._host = host + def __init__(self, hass): + """Initialise the RoonHub.""" + self._hass = hass + + async def discover(self): + """Try and discover roon servers.""" + + def get_discovered_servers(discovery): + servers = discovery.all() + discovery.stop() + return servers + + discovery = RoonDiscovery(None) + servers = await self._hass.async_add_executor_job( + get_discovered_servers, discovery + ) + _LOGGER.debug("Servers = %s", servers) + return servers + + async def authenticate(self, host, servers): + """Authenticate with one or more roon servers.""" + + def stop_apis(apis): + for api in apis: + api.stop() - async def authenticate(self, hass) -> bool: - """Test if we can authenticate with the host.""" token = None + core_id = None secs = 0 - roonapi = RoonApi(ROON_APPINFO, None, self._host, blocking_init=False) - while secs < TIMEOUT: - token = roonapi.token + if host is None: + apis = [ + RoonApi(ROON_APPINFO, None, server[0], server[1], blocking_init=False) + for server in servers + ] + else: + apis = [RoonApi(ROON_APPINFO, None, host, blocking_init=False)] + + while secs <= TIMEOUT: + # Roon can discover multiple devices - not all of which are proper servers, so try and authenticate with them all. + # The user will only enable one - so look for a valid token + auth_api = [api for api in apis if api.token is not None] + secs += AUTHENTICATE_TIMEOUT - if token: + if auth_api: + core_id = auth_api[0].core_id + token = auth_api[0].token break + await asyncio.sleep(AUTHENTICATE_TIMEOUT) - token = roonapi.token - roonapi.stop() - return token + await self._hass.async_add_executor_job(stop_apis, apis) + + return (token, core_id) -async def authenticate(hass: core.HomeAssistant, host): +async def discover(hass): """Connect and authenticate home assistant.""" - hub = RoonHub(host) - token = await hub.authenticate(hass) + hub = RoonHub(hass) + servers = await hub.discover() + + return servers + + +async def authenticate(hass: core.HomeAssistant, host, servers): + """Connect and authenticate home assistant.""" + + hub = RoonHub(hass) + (token, core_id) = await hub.authenticate(host, servers) if token is None: raise InvalidAuth - return {CONF_HOST: host, CONF_API_KEY: token} + return {CONF_HOST: host, CONF_ROON_ID: core_id, CONF_API_KEY: token} class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): @@ -66,20 +110,20 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the Roon flow.""" self._host = None + self._servers = [] async def async_step_user(self, user_input=None): """Handle getting host details from the user.""" errors = {} + self._servers = await discover(self.hass) + + # We discovered one or more roon - so skip to authentication + if self._servers: + return await self.async_step_link() + if user_input is not None: self._host = user_input["host"] - existing = { - entry.data[CONF_HOST] for entry in self._async_current_entries() - } - if self._host in existing: - errors["base"] = "duplicate_entry" - return self.async_show_form(step_id="user", errors=errors) - return await self.async_step_link() return self.async_show_form( @@ -92,7 +136,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: try: - info = await authenticate(self.hass, self._host) + info = await authenticate(self.hass, self._host, self._servers) except InvalidAuth: errors["base"] = "invalid_auth" except Exception: # pylint: disable=broad-except diff --git a/homeassistant/components/roon/const.py b/homeassistant/components/roon/const.py index dc11d4167a7..7c9cd6c4999 100644 --- a/homeassistant/components/roon/const.py +++ b/homeassistant/components/roon/const.py @@ -4,6 +4,8 @@ AUTHENTICATE_TIMEOUT = 5 DOMAIN = "roon" +CONF_ROON_ID = "roon_server_id" + DATA_CONFIGS = "roon_configs" DEFAULT_NAME = "Roon Labs Music Player" diff --git a/homeassistant/components/roon/manifest.json b/homeassistant/components/roon/manifest.json index 4bd5903253a..0d5d0c131ae 100644 --- a/homeassistant/components/roon/manifest.json +++ b/homeassistant/components/roon/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roon", "requirements": [ - "roonapi==0.0.28" + "roonapi==0.0.31" ], "codeowners": [ "@pavoni" diff --git a/homeassistant/components/roon/media_player.py b/homeassistant/components/roon/media_player.py index e0ae86b781a..8abcba189da 100644 --- a/homeassistant/components/roon/media_player.py +++ b/homeassistant/components/roon/media_player.py @@ -1,6 +1,7 @@ """MediaPlayer platform for Roon integration.""" import logging +from roonapi import split_media_path import voluptuous as vol from homeassistant.components.media_player import MediaPlayerEntity @@ -120,8 +121,6 @@ class RoonDevice(MediaPlayerEntity): self._last_position_update = None self._supports_standby = False self._state = STATE_IDLE - self._last_playlist = None - self._last_media = None self._unique_id = None self._zone_id = None self._output_id = None @@ -176,7 +175,7 @@ class RoonDevice(MediaPlayerEntity): "name": self.name, "manufacturer": "RoonLabs", "model": dev_model, - "via_hub": (DOMAIN, self._server.host), + "via_hub": (DOMAIN, self._server.roon_id), } def update_data(self, player_data=None): @@ -354,11 +353,6 @@ class RoonDevice(MediaPlayerEntity): """Album artist of current playing media (Music track only).""" return self._media_artist - @property - def media_playlist(self): - """Title of Playlist currently playing.""" - return self._last_playlist - @property def media_image_url(self): """Image url of current playing media.""" @@ -481,32 +475,21 @@ class RoonDevice(MediaPlayerEntity): def play_media(self, media_type, media_id, **kwargs): """Send the play_media command to the media player.""" - # Roon itself doesn't support playback of media by filename/url so this a bit of a workaround. - media_type = media_type.lower() - if media_type == "radio": - if self._server.roonapi.play_radio(self.zone_id, media_id): - self._last_playlist = media_id - self._last_media = media_id - elif media_type == "playlist": - if self._server.roonapi.play_playlist( - self.zone_id, media_id, shuffle=False - ): - self._last_playlist = media_id - elif media_type == "shuffleplaylist": - if self._server.roonapi.play_playlist(self.zone_id, media_id, shuffle=True): - self._last_playlist = media_id - elif media_type == "queueplaylist": - self._server.roonapi.queue_playlist(self.zone_id, media_id) - elif media_type == "genre": - self._server.roonapi.play_genre(self.zone_id, media_id) - elif media_type in ("library", "track"): + + _LOGGER.debug("Playback request for %s / %s", media_type, media_id) + if media_type in ("library", "track"): + # media_id is a roon browser id self._server.roonapi.play_id(self.zone_id, media_id) else: - _LOGGER.error( - "Playback requested of unsupported type: %s --> %s", - media_type, - media_id, - ) + # media_id is a path matching the Roon menu structure + path_list = split_media_path(media_id) + if not self._server.roonapi.play_media(self.zone_id, path_list): + _LOGGER.error( + "Playback request for %s / %s / %s was unsuccessful", + media_type, + media_id, + path_list, + ) def join(self, join_ids): """Add another Roon player to this player's join group.""" diff --git a/homeassistant/components/roon/server.py b/homeassistant/components/roon/server.py index 0229f3492be..d5b8d81c2aa 100644 --- a/homeassistant/components/roon/server.py +++ b/homeassistant/components/roon/server.py @@ -8,7 +8,7 @@ from homeassistant.const import CONF_API_KEY, CONF_HOST from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.util.dt import utcnow -from .const import ROON_APPINFO +from .const import CONF_ROON_ID, ROON_APPINFO _LOGGER = logging.getLogger(__name__) FULL_SYNC_INTERVAL = 30 @@ -22,28 +22,33 @@ class RoonServer: self.config_entry = config_entry self.hass = hass self.roonapi = None + self.roon_id = None self.all_player_ids = set() self.all_playlists = [] self.offline_devices = set() self._exit = False self._roon_name_by_id = {} - @property - def host(self): - """Return the host of this server.""" - return self.config_entry.data[CONF_HOST] - async def async_setup(self, tries=0): - """Set up a roon server based on host parameter.""" - host = self.host + """Set up a roon server based on config parameters.""" hass = self.hass + # Host will be None for configs using discovery + host = self.config_entry.data[CONF_HOST] token = self.config_entry.data[CONF_API_KEY] - _LOGGER.debug("async_setup: %s %s", token, host) - self.roonapi = RoonApi(ROON_APPINFO, token, host, blocking_init=False) + # Default to None for compatibility with older configs + core_id = self.config_entry.data.get(CONF_ROON_ID) + _LOGGER.debug("async_setup: host=%s core_id=%s token=%s", host, core_id, token) + + self.roonapi = RoonApi( + ROON_APPINFO, token, host, blocking_init=False, core_id=core_id + ) self.roonapi.register_state_callback( self.roonapi_state_callback, event_filter=["zones_changed"] ) + # Default to 'host' for compatibility with older configs without core_id + self.roon_id = core_id if core_id is not None else host + # initialize media_player platform hass.async_create_task( hass.config_entries.async_forward_entry_setup( @@ -152,11 +157,11 @@ class RoonServer: new_dict = zone.copy() new_dict.update(output) new_dict.pop("outputs") - new_dict["host"] = self.host + new_dict["roon_id"] = self.roon_id new_dict["is_synced"] = len(zone["outputs"]) > 1 new_dict["zone_name"] = zone["display_name"] new_dict["display_name"] = output["display_name"] new_dict["last_changed"] = utcnow() # we don't use the zone_id or output_id for now as unique id as I've seen cases were it changes for some reason - new_dict["dev_id"] = f"roon_{self.host}_{output['display_name']}" + new_dict["dev_id"] = f"roon_{self.roon_id}_{output['display_name']}" return new_dict diff --git a/homeassistant/components/roon/strings.json b/homeassistant/components/roon/strings.json index 8164f47ade8..565a66a1320 100644 --- a/homeassistant/components/roon/strings.json +++ b/homeassistant/components/roon/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "description": "Please enter your Roon server Hostname or IP.", + "description": "Could not discover Roon server, please enter your the Hostname or IP.", "data": { "host": "[%key:common::config_flow::data::host%]" } @@ -14,8 +14,7 @@ }, "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "unknown": "[%key:common::config_flow::error::unknown%]", - "duplicate_entry": "That host has already been added." + "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" diff --git a/homeassistant/components/roon/translations/ca.json b/homeassistant/components/roon/translations/ca.json index 3a1de2208b6..ef32dd00e75 100644 --- a/homeassistant/components/roon/translations/ca.json +++ b/homeassistant/components/roon/translations/ca.json @@ -17,7 +17,7 @@ "data": { "host": "Amfitri\u00f3" }, - "description": "Introdueix el nom d'amfitri\u00f3 o la IP del servidor Roon" + "description": "No s'ha pogut descobrir el servidor Roon, introdueix el nom d'amfitri\u00f3 o la IP." } } } diff --git a/homeassistant/components/roon/translations/cs.json b/homeassistant/components/roon/translations/cs.json index a15e75066a9..fd01ed1cd25 100644 --- a/homeassistant/components/roon/translations/cs.json +++ b/homeassistant/components/roon/translations/cs.json @@ -16,8 +16,7 @@ "user": { "data": { "host": "Hostitel" - }, - "description": "Zadejte pros\u00edm n\u00e1zev hostitele nebo IP adresu va\u0161eho Roon serveru." + } } } } diff --git a/homeassistant/components/roon/translations/de.json b/homeassistant/components/roon/translations/de.json index 9918e38670a..4416589a23e 100644 --- a/homeassistant/components/roon/translations/de.json +++ b/homeassistant/components/roon/translations/de.json @@ -1,8 +1,19 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { "duplicate_entry": "Dieser Host wurde bereits hinzugef\u00fcgt.", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/roon/translations/en.json b/homeassistant/components/roon/translations/en.json index 99f2b65bd13..b763fbb1e0c 100644 --- a/homeassistant/components/roon/translations/en.json +++ b/homeassistant/components/roon/translations/en.json @@ -17,7 +17,7 @@ "data": { "host": "Host" }, - "description": "Please enter your Roon server Hostname or IP." + "description": "Could not discover Roon server, please enter your the Hostname or IP." } } } diff --git a/homeassistant/components/roon/translations/et.json b/homeassistant/components/roon/translations/et.json index dfe3ad53f48..e29b1ccc6c6 100644 --- a/homeassistant/components/roon/translations/et.json +++ b/homeassistant/components/roon/translations/et.json @@ -17,7 +17,7 @@ "data": { "host": "" }, - "description": "Sisesta oma Rooni serveri hostinimi v\u00f5i IP." + "description": "Rooni serverit ei leitud. Sisesta oma Rooni serveri hostinimi v\u00f5i IP." } } } diff --git a/homeassistant/components/roon/translations/it.json b/homeassistant/components/roon/translations/it.json index 5f63482c3c3..e0450af9d39 100644 --- a/homeassistant/components/roon/translations/it.json +++ b/homeassistant/components/roon/translations/it.json @@ -17,7 +17,7 @@ "data": { "host": "Host" }, - "description": "Inserisci il nome host o l'IP del tuo server Roon." + "description": "Impossibile individuare il server Roon, inserire l'hostname o l'IP." } } } diff --git a/homeassistant/components/roon/translations/no.json b/homeassistant/components/roon/translations/no.json index 9067e2c6f53..e872e03a69d 100644 --- a/homeassistant/components/roon/translations/no.json +++ b/homeassistant/components/roon/translations/no.json @@ -17,7 +17,7 @@ "data": { "host": "Vert" }, - "description": "Vennligst skriv inn Roon-serverens vertsnavn eller IP." + "description": "Kunne ikke oppdage Roon-serveren. Angi vertsnavnet eller IP-adressen." } } } diff --git a/homeassistant/components/roon/translations/pl.json b/homeassistant/components/roon/translations/pl.json index e63c5f6b55c..d763fc12bd2 100644 --- a/homeassistant/components/roon/translations/pl.json +++ b/homeassistant/components/roon/translations/pl.json @@ -17,7 +17,7 @@ "data": { "host": "Nazwa hosta lub adres IP" }, - "description": "Wprowad\u017a nazw\u0119 hosta lub adres IP swojego serwera Roon." + "description": "Nie wykryto serwera Roon, wprowad\u017a nazw\u0119 hosta lub adres IP." } } } diff --git a/homeassistant/components/roon/translations/ru.json b/homeassistant/components/roon/translations/ru.json index abfbea2ccde..187151affe2 100644 --- a/homeassistant/components/roon/translations/ru.json +++ b/homeassistant/components/roon/translations/ru.json @@ -17,7 +17,7 @@ "data": { "host": "\u0425\u043e\u0441\u0442" }, - "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Roon" + "description": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440 Roon, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0445\u043e\u0441\u0442\u0430 \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441." } } } diff --git a/homeassistant/components/roon/translations/tr.json b/homeassistant/components/roon/translations/tr.json new file mode 100644 index 00000000000..97241919c9b --- /dev/null +++ b/homeassistant/components/roon/translations/tr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "duplicate_entry": "Bu ana bilgisayar zaten eklendi.", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "link": { + "description": "Roon'da HomeAssistant\u0131 yetkilendirmelisiniz. G\u00f6nder'e t\u0131klad\u0131ktan sonra, Roon Core uygulamas\u0131na gidin, Ayarlar'\u0131 a\u00e7\u0131n ve Uzant\u0131lar sekmesinde HomeAssistant'\u0131 etkinle\u015ftirin.", + "title": "Roon'da HomeAssistant'\u0131 Yetkilendirme" + }, + "user": { + "data": { + "host": "Ana Bilgisayar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roon/translations/uk.json b/homeassistant/components/roon/translations/uk.json new file mode 100644 index 00000000000..91a530787ae --- /dev/null +++ b/homeassistant/components/roon/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "duplicate_entry": "\u0426\u0435\u0439 \u0445\u043e\u0441\u0442 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0438\u0439.", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "link": { + "description": "\u041f\u0456\u0441\u043b\u044f \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f \u043a\u043d\u043e\u043f\u043a\u0438 \u00ab\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u0438\u00bb \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0432 \u0434\u043e\u0434\u0430\u0442\u043e\u043a Roon Core, \u0432\u0456\u0434\u043a\u0440\u0438\u0439\u0442\u0435 \u00ab\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u00bb \u0456 \u0443\u0432\u0456\u043c\u043a\u043d\u0456\u0442\u044c HomeAssistant \u043d\u0430 \u0432\u043a\u043b\u0430\u0434\u0446\u0456 \u00ab\u0420\u043e\u0437\u0448\u0438\u0440\u0435\u043d\u043d\u044f\u00bb.", + "title": "Roon" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043d\u0430\u0437\u0432\u0443 \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Roon" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roon/translations/zh-Hant.json b/homeassistant/components/roon/translations/zh-Hant.json index f34bce445f7..39099753f39 100644 --- a/homeassistant/components/roon/translations/zh-Hant.json +++ b/homeassistant/components/roon/translations/zh-Hant.json @@ -17,7 +17,7 @@ "data": { "host": "\u4e3b\u6a5f\u7aef" }, - "description": "\u8acb\u8f38\u5165 Roon \u4f3a\u670d\u5668\u4e3b\u6a5f\u540d\u7a31\u6216 IP\u3002" + "description": "\u627e\u4e0d\u5230 Roon \u4f3a\u670d\u5668\uff0c\u8acb\u8f38\u5165\u4e3b\u6a5f\u540d\u7a31\u6216 IP\u3002" } } } diff --git a/homeassistant/components/rpi_power/translations/de.json b/homeassistant/components/rpi_power/translations/de.json new file mode 100644 index 00000000000..9f3851f0c2b --- /dev/null +++ b/homeassistant/components/rpi_power/translations/de.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "step": { + "confirm": { + "description": "M\u00f6chtest du mit der Einrichtung beginnen?" + } + } + }, + "title": "Raspberry Pi Stromversorgungspr\u00fcfer" +} \ No newline at end of file diff --git a/homeassistant/components/rpi_power/translations/lb.json b/homeassistant/components/rpi_power/translations/lb.json index 3e145432bae..e4bb7389339 100644 --- a/homeassistant/components/rpi_power/translations/lb.json +++ b/homeassistant/components/rpi_power/translations/lb.json @@ -3,6 +3,11 @@ "abort": { "no_devices_found": "Kann d\u00e9i Systemklass fir d\u00ebs noutwendeg Komponent net fannen, stell s\u00e9cher dass de Kernel rezent ass an d'Hardware \u00ebnnerst\u00ebtzt g\u00ebtt.", "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech." + }, + "step": { + "confirm": { + "description": "Soll den Ariichtungs Prozess gestart ginn?" + } } }, "title": "Raspberry Pi Netzdeel Checker" diff --git a/homeassistant/components/rpi_power/translations/tr.json b/homeassistant/components/rpi_power/translations/tr.json new file mode 100644 index 00000000000..f1dfcf16667 --- /dev/null +++ b/homeassistant/components/rpi_power/translations/tr.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "confirm": { + "description": "Kuruluma ba\u015flamak ister misiniz?" + } + } + }, + "title": "Raspberry Pi G\u00fc\u00e7 Kayna\u011f\u0131 Denetleyicisi" +} \ No newline at end of file diff --git a/homeassistant/components/rpi_power/translations/uk.json b/homeassistant/components/rpi_power/translations/uk.json new file mode 100644 index 00000000000..b60160e1c4e --- /dev/null +++ b/homeassistant/components/rpi_power/translations/uk.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u043d\u0430\u0439\u0442\u0438 \u0441\u0438\u0441\u0442\u0435\u043c\u043d\u0438\u0439 \u043a\u043b\u0430\u0441, \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u0438\u0439 \u0434\u043b\u044f \u0440\u043e\u0431\u043e\u0442\u0438 \u0446\u044c\u043e\u0433\u043e \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430. \u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0443 \u0412\u0430\u0441 \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d\u043e \u043d\u0430\u0439\u043d\u043e\u0432\u0456\u0448\u0435 \u044f\u0434\u0440\u043e \u0456 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0435 \u043e\u0431\u043b\u0430\u0434\u043d\u0430\u043d\u043d\u044f.", + "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." + }, + "step": { + "confirm": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?" + } + } + }, + "title": "Raspberry Pi power supply checker" +} \ No newline at end of file diff --git a/homeassistant/components/ruckus_unleashed/translations/de.json b/homeassistant/components/ruckus_unleashed/translations/de.json index ae15ec058b5..625c7372347 100644 --- a/homeassistant/components/ruckus_unleashed/translations/de.json +++ b/homeassistant/components/ruckus_unleashed/translations/de.json @@ -4,12 +4,14 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { "user": { "data": { + "host": "Host", "password": "Passwort", "username": "Benutzername" } diff --git a/homeassistant/components/ruckus_unleashed/translations/tr.json b/homeassistant/components/ruckus_unleashed/translations/tr.json new file mode 100644 index 00000000000..40c9c39b967 --- /dev/null +++ b/homeassistant/components/ruckus_unleashed/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ruckus_unleashed/translations/uk.json b/homeassistant/components/ruckus_unleashed/translations/uk.json new file mode 100644 index 00000000000..2df11f74455 --- /dev/null +++ b/homeassistant/components/ruckus_unleashed/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/de.json b/homeassistant/components/samsungtv/translations/de.json index e3354267630..3ba569c87db 100644 --- a/homeassistant/components/samsungtv/translations/de.json +++ b/homeassistant/components/samsungtv/translations/de.json @@ -2,9 +2,9 @@ "config": { "abort": { "already_configured": "Dieser Samsung TV ist bereits konfiguriert", - "already_in_progress": "Der Konfigurationsablauf f\u00fcr Samsung TV wird bereits ausgef\u00fchrt.", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt", "auth_missing": "Home Assistant ist nicht berechtigt, eine Verbindung zu diesem Samsung TV herzustellen. \u00dcberpr\u00fcfe die Einstellungen deines Fernsehger\u00e4ts, um Home Assistant zu autorisieren.", - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", "not_supported": "Dieses Samsung TV-Ger\u00e4t wird derzeit nicht unterst\u00fctzt." }, "flow_title": "Samsung TV: {model}", diff --git a/homeassistant/components/samsungtv/translations/tr.json b/homeassistant/components/samsungtv/translations/tr.json index 50e6b21d120..6b3900e9aa5 100644 --- a/homeassistant/components/samsungtv/translations/tr.json +++ b/homeassistant/components/samsungtv/translations/tr.json @@ -4,13 +4,17 @@ "already_configured": "Bu Samsung TV zaten ayarlanm\u0131\u015f.", "already_in_progress": "Samsung TV ayar\u0131 zaten s\u00fcr\u00fcyor.", "auth_missing": "Home Assistant'\u0131n bu Samsung TV'ye ba\u011flanma izni yok. Home Assistant'\u0131 yetkilendirmek i\u00e7in l\u00fctfen TV'nin ayarlar\u0131n\u0131 kontrol et.", + "cannot_connect": "Ba\u011flanma hatas\u0131", "not_supported": "Bu Samsung TV cihaz\u0131 \u015fu anda desteklenmiyor." }, "flow_title": "Samsung TV: {model}", "step": { + "confirm": { + "title": "Samsung TV" + }, "user": { "data": { - "host": "Host veya IP adresi", + "host": "Ana Bilgisayar", "name": "Ad" }, "description": "Samsung TV bilgilerini gir. Daha \u00f6nce hi\u00e7 Home Assistant'a ba\u011flamad\u0131ysan, TV'nde izin isteyen bir pencere g\u00f6receksindir." diff --git a/homeassistant/components/samsungtv/translations/uk.json b/homeassistant/components/samsungtv/translations/uk.json new file mode 100644 index 00000000000..83bb18e76f1 --- /dev/null +++ b/homeassistant/components/samsungtv/translations/uk.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "auth_missing": "Home Assistant \u043d\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u0438\u0439 \u0434\u043b\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u0446\u044c\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "not_supported": "\u0426\u044f \u043c\u043e\u0434\u0435\u043b\u044c \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 \u0432 \u0434\u0430\u043d\u0438\u0439 \u0447\u0430\u0441 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f." + }, + "flow_title": "Samsung TV: {model}", + "step": { + "confirm": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 Samsung {model}? \u042f\u043a\u0449\u043e \u0446\u0435\u0439 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 \u0440\u0430\u043d\u0456\u0448\u0435 \u043d\u0435 \u0431\u0443\u0432 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439 \u0434\u043e Home Assistant, \u043d\u0430 \u0435\u043a\u0440\u0430\u043d\u0456 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 \u043c\u0430\u0454 \u0437'\u044f\u0432\u0438\u0442\u0438\u0441\u044f \u0441\u043f\u043b\u0438\u0432\u0430\u044e\u0447\u0435 \u0432\u0456\u043a\u043d\u043e \u0456\u0437 \u0437\u0430\u043f\u0438\u0442\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457. \u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430, \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0456 \u0432\u0440\u0443\u0447\u043d\u0443, \u0431\u0443\u0434\u0443\u0442\u044c \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0438\u0441\u0430\u043d\u0456.", + "title": "\u0422\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 Samsung" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 Samsung. \u042f\u043a\u0449\u043e \u0446\u0435\u0439 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 \u0440\u0430\u043d\u0456\u0448\u0435 \u043d\u0435 \u0431\u0443\u0432 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439 \u0434\u043e Home Assistant, \u043d\u0430 \u0435\u043a\u0440\u0430\u043d\u0456 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0430 \u043c\u0430\u0454 \u0437'\u044f\u0432\u0438\u0442\u0438\u0441\u044f \u0441\u043f\u043b\u0438\u0432\u0430\u044e\u0447\u0435 \u0432\u0456\u043a\u043d\u043e \u0456\u0437 \u0437\u0430\u043f\u0438\u0442\u043e\u043c \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/script/translations/uk.json b/homeassistant/components/script/translations/uk.json index bfff0258c66..ee494e264ae 100644 --- a/homeassistant/components/script/translations/uk.json +++ b/homeassistant/components/script/translations/uk.json @@ -5,5 +5,5 @@ "on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e" } }, - "title": "\u0421\u0446\u0435\u043d\u0430\u0440\u0456\u0439" + "title": "\u0421\u043a\u0440\u0438\u043f\u0442" } \ No newline at end of file diff --git a/homeassistant/components/season/translations/sensor.uk.json b/homeassistant/components/season/translations/sensor.uk.json index 2c694e287b1..fa79d3cff07 100644 --- a/homeassistant/components/season/translations/sensor.uk.json +++ b/homeassistant/components/season/translations/sensor.uk.json @@ -1,5 +1,11 @@ { "state": { + "season__season": { + "autumn": "\u041e\u0441\u0456\u043d\u044c", + "spring": "\u0412\u0435\u0441\u043d\u0430", + "summer": "\u041b\u0456\u0442\u043e", + "winter": "\u0417\u0438\u043c\u0430" + }, "season__season__": { "autumn": "\u041e\u0441\u0456\u043d\u044c", "spring": "\u0412\u0435\u0441\u043d\u0430", diff --git a/homeassistant/components/sendgrid/manifest.json b/homeassistant/components/sendgrid/manifest.json index d09ec684e52..1a93040837a 100644 --- a/homeassistant/components/sendgrid/manifest.json +++ b/homeassistant/components/sendgrid/manifest.json @@ -2,6 +2,6 @@ "domain": "sendgrid", "name": "SendGrid", "documentation": "https://www.home-assistant.io/integrations/sendgrid", - "requirements": ["sendgrid==6.4.6"], + "requirements": ["sendgrid==6.5.0"], "codeowners": [] } diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index f0b20f27a55..bd132f1f983 100644 --- a/homeassistant/components/sense/manifest.json +++ b/homeassistant/components/sense/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/sense", "requirements": ["sense_energy==0.8.1"], "codeowners": ["@kbickar"], - "config_flow": true + "config_flow": true, + "dhcp": [{"hostname":"sense-*","macaddress":"009D6B*"}, {"hostname":"sense-*","macaddress":"DCEFCA*"}] } diff --git a/homeassistant/components/sense/translations/de.json b/homeassistant/components/sense/translations/de.json index de9e6877f25..9d4845ece79 100644 --- a/homeassistant/components/sense/translations/de.json +++ b/homeassistant/components/sense/translations/de.json @@ -4,7 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, diff --git a/homeassistant/components/sense/translations/tr.json b/homeassistant/components/sense/translations/tr.json new file mode 100644 index 00000000000..0e335265325 --- /dev/null +++ b/homeassistant/components/sense/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "email": "E-posta", + "password": "Parola" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/translations/uk.json b/homeassistant/components/sense/translations/uk.json new file mode 100644 index 00000000000..8eac9c9d4ab --- /dev/null +++ b/homeassistant/components/sense/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "email": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "title": "Sense Energy Monitor" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/device_condition.py b/homeassistant/components/sensor/device_condition.py index d58381a2f40..a9d44f2f860 100644 --- a/homeassistant/components/sensor/device_condition.py +++ b/homeassistant/components/sensor/device_condition.py @@ -169,7 +169,10 @@ async def async_get_condition_capabilities(hass, config): ) if not state or not unit_of_measurement: - raise InvalidDeviceAutomationConfig + raise InvalidDeviceAutomationConfig( + "No state or unit of measurement found for " + f"condition entity {config[CONF_ENTITY_ID]}" + ) return { "extra_fields": vol.Schema( diff --git a/homeassistant/components/sensor/device_trigger.py b/homeassistant/components/sensor/device_trigger.py index 77f6afd9acf..86dda53cd2b 100644 --- a/homeassistant/components/sensor/device_trigger.py +++ b/homeassistant/components/sensor/device_trigger.py @@ -168,7 +168,10 @@ async def async_get_trigger_capabilities(hass, config): ) if not state or not unit_of_measurement: - raise InvalidDeviceAutomationConfig + raise InvalidDeviceAutomationConfig( + "No state or unit of measurement found for " + f"trigger entity {config[CONF_ENTITY_ID]}" + ) return { "extra_fields": vol.Schema( diff --git a/homeassistant/components/sensor/significant_change.py b/homeassistant/components/sensor/significant_change.py new file mode 100644 index 00000000000..2c281c0a046 --- /dev/null +++ b/homeassistant/components/sensor/significant_change.py @@ -0,0 +1,45 @@ +"""Helper to test significant sensor state changes.""" +from typing import Any, Optional, Union + +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, + TEMP_FAHRENHEIT, +) +from homeassistant.core import HomeAssistant, callback + +from . import DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE + + +@callback +def async_check_significant_change( + hass: HomeAssistant, + old_state: str, + old_attrs: dict, + new_state: str, + new_attrs: dict, + **kwargs: Any, +) -> Optional[bool]: + """Test if state significantly changed.""" + device_class = new_attrs.get(ATTR_DEVICE_CLASS) + + if device_class is None: + return None + + if device_class == DEVICE_CLASS_TEMPERATURE: + if new_attrs.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_FAHRENHEIT: + change: Union[float, int] = 1 + else: + change = 0.5 + + old_value = float(old_state) + new_value = float(new_state) + return abs(old_value - new_value) >= change + + if device_class in (DEVICE_CLASS_BATTERY, DEVICE_CLASS_HUMIDITY): + old_value = float(old_state) + new_value = float(new_state) + + return abs(old_value - new_value) >= 1 + + return None diff --git a/homeassistant/components/sensor/translations/tr.json b/homeassistant/components/sensor/translations/tr.json index 3bf1ba6f368..feca40991ee 100644 --- a/homeassistant/components/sensor/translations/tr.json +++ b/homeassistant/components/sensor/translations/tr.json @@ -1,4 +1,31 @@ { + "device_automation": { + "condition_type": { + "is_current": "Mevcut {entity_name} ak\u0131m\u0131", + "is_energy": "Mevcut {entity_name} enerjisi", + "is_power_factor": "Mevcut {entity_name} g\u00fc\u00e7 fakt\u00f6r\u00fc", + "is_signal_strength": "Mevcut {entity_name} sinyal g\u00fcc\u00fc", + "is_temperature": "Mevcut {entity_name} s\u0131cakl\u0131\u011f\u0131", + "is_timestamp": "Mevcut {entity_name} zaman damgas\u0131", + "is_value": "Mevcut {entity_name} de\u011feri", + "is_voltage": "Mevcut {entity_name} voltaj\u0131" + }, + "trigger_type": { + "battery_level": "{entity_name} pil seviyesi de\u011fi\u015fiklikleri", + "current": "{entity_name} ak\u0131m de\u011fi\u015fiklikleri", + "energy": "{entity_name} enerji de\u011fi\u015fiklikleri", + "humidity": "{entity_name} nem de\u011fi\u015fiklikleri", + "illuminance": "{entity_name} ayd\u0131nlatma de\u011fi\u015fiklikleri", + "power": "{entity_name} g\u00fc\u00e7 de\u011fi\u015fiklikleri", + "power_factor": "{entity_name} g\u00fc\u00e7 fakt\u00f6r\u00fc de\u011fi\u015fiklikleri", + "pressure": "{entity_name} bas\u0131n\u00e7 de\u011fi\u015fiklikleri", + "signal_strength": "{entity_name} sinyal g\u00fcc\u00fc de\u011fi\u015fiklikleri", + "temperature": "{entity_name} s\u0131cakl\u0131k de\u011fi\u015fiklikleri", + "timestamp": "{entity_name} zaman damgas\u0131 de\u011fi\u015fiklikleri", + "value": "{entity_name} de\u011fer de\u011fi\u015fiklikleri", + "voltage": "{entity_name} voltaj de\u011fi\u015fiklikleri" + } + }, "state": { "_": { "off": "Kapal\u0131", diff --git a/homeassistant/components/sensor/translations/uk.json b/homeassistant/components/sensor/translations/uk.json index 391415409f5..9e6148c3b8c 100644 --- a/homeassistant/components/sensor/translations/uk.json +++ b/homeassistant/components/sensor/translations/uk.json @@ -1,7 +1,34 @@ { "device_automation": { "condition_type": { - "is_battery_level": "\u041f\u043e\u0442\u043e\u0447\u043d\u0438\u0439 \u0440\u0456\u0432\u0435\u043d\u044c \u0437\u0430\u0440\u044f\u0434\u0443 \u0430\u043a\u0443\u043c\u0443\u043b\u044f\u0442\u043e\u0440\u0430 {entity_name}" + "is_battery_level": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "is_current": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0441\u0438\u043b\u0438 \u0441\u0442\u0440\u0443\u043c\u0443", + "is_energy": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u043e\u0442\u0443\u0436\u043d\u043e\u0441\u0442\u0456", + "is_humidity": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "is_illuminance": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "is_power": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "is_power_factor": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043a\u043e\u0435\u0444\u0456\u0446\u0456\u0454\u043d\u0442\u0430 \u043f\u043e\u0442\u0443\u0436\u043d\u043e\u0441\u0442\u0456", + "is_pressure": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "is_signal_strength": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "is_temperature": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "is_timestamp": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "is_value": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "is_voltage": "{entity_name} \u043c\u0430\u0454 \u043f\u043e\u0442\u043e\u0447\u043d\u0435 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043d\u0430\u043f\u0440\u0443\u0433\u0438" + }, + "trigger_type": { + "battery_level": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "current": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0441\u0438\u043b\u0438 \u0441\u0442\u0440\u0443\u043c\u0443", + "energy": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043f\u043e\u0442\u0443\u0436\u043d\u043e\u0441\u0442\u0456", + "humidity": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "illuminance": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "power": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "power_factor": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u043a\u043e\u0435\u0444\u0456\u0446\u0456\u0454\u043d\u0442 \u043f\u043e\u0442\u0443\u0436\u043d\u043e\u0441\u0442\u0456", + "pressure": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "signal_strength": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "temperature": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "timestamp": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "value": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f", + "voltage": "{entity_name} \u0437\u043c\u0456\u043d\u044e\u0454 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u043d\u0430\u043f\u0440\u0443\u0433\u0438" } }, "state": { @@ -10,5 +37,5 @@ "on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e" } }, - "title": "\u0414\u0430\u0442\u0447\u0438\u043a" + "title": "\u0421\u0435\u043d\u0441\u043e\u0440" } \ No newline at end of file diff --git a/homeassistant/components/sentry/manifest.json b/homeassistant/components/sentry/manifest.json index be07586cebd..da5294b9258 100644 --- a/homeassistant/components/sentry/manifest.json +++ b/homeassistant/components/sentry/manifest.json @@ -3,6 +3,6 @@ "name": "Sentry", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sentry", - "requirements": ["sentry-sdk==0.19.4"], + "requirements": ["sentry-sdk==0.19.5"], "codeowners": ["@dcramer", "@frenck"] } diff --git a/homeassistant/components/sentry/translations/de.json b/homeassistant/components/sentry/translations/de.json index c36bbf258b0..8fbcfc1eaa2 100644 --- a/homeassistant/components/sentry/translations/de.json +++ b/homeassistant/components/sentry/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, "error": { "bad_dsn": "Ung\u00fcltiger DSN", "unknown": "Unerwarteter Fehler" diff --git a/homeassistant/components/sentry/translations/tr.json b/homeassistant/components/sentry/translations/tr.json new file mode 100644 index 00000000000..4dab23fbd94 --- /dev/null +++ b/homeassistant/components/sentry/translations/tr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "bad_dsn": "Ge\u00e7ersiz DSN", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "dsn": "DSN" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "environment": "Ortam\u0131n iste\u011fe ba\u011fl\u0131 ad\u0131." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sentry/translations/uk.json b/homeassistant/components/sentry/translations/uk.json new file mode 100644 index 00000000000..01da0308851 --- /dev/null +++ b/homeassistant/components/sentry/translations/uk.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "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." + }, + "error": { + "bad_dsn": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 DSN.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "dsn": "DSN" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0412\u0430\u0448 DSN Sentry", + "title": "Sentry" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "environment": "\u041d\u0430\u0437\u0432\u0430", + "event_custom_components": "\u0412\u0456\u0434\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u0438 \u043f\u043e\u0434\u0456\u0457 \u0437 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0446\u044c\u043a\u0438\u0445 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0456\u0432", + "event_handled": "\u0412\u0456\u0434\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u0438 \u043e\u0431\u0440\u043e\u0431\u043b\u0435\u043d\u0456 \u043f\u043e\u0434\u0456\u0457", + "event_third_party_packages": "\u0412\u0456\u0434\u043f\u0440\u0430\u0432\u043b\u044f\u0442\u0438 \u043f\u043e\u0434\u0456\u0457 \u0437 \u0441\u0442\u043e\u0440\u043e\u043d\u043d\u0456\u0445 \u043f\u0430\u043a\u0435\u0442\u0456\u0432", + "logging_event_level": "\u0417\u0430\u043f\u0438\u0441\u0443\u0432\u0430\u0442\u0438 \u0436\u0443\u0440\u043d\u0430\u043b\u0438 \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u043e\u0432\u0430\u043d\u0438\u0445 \u043f\u043e\u0434\u0456\u0439", + "logging_level": "\u0417\u0430\u043f\u0438\u0441\u0443\u0432\u0430\u0442\u0438 \u0436\u0443\u0440\u043d\u0430\u043b\u0438 \u0443 \u0432\u0438\u0433\u043b\u044f\u0434\u0456 \u043d\u0430\u0432\u0456\u0433\u0430\u0446\u0456\u0439\u043d\u0438\u0445 \u043b\u0430\u043d\u0446\u044e\u0436\u043a\u0456\u0432", + "tracing": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0432\u0456\u0434\u0441\u0442\u0435\u0436\u0435\u043d\u043d\u044f \u043f\u0440\u043e\u0434\u0443\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u0456", + "tracing_sample_rate": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u0434\u0438\u0441\u043a\u0440\u0435\u0442\u0438\u0437\u0430\u0446\u0456\u0457 \u0442\u0440\u0430\u0441\u0443\u0432\u0430\u043d\u043d\u044f; \u0432\u0456\u0434 0,0 \u0434\u043e 1,0 (1,0 = 100%)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/seven_segments/manifest.json b/homeassistant/components/seven_segments/manifest.json index 4996ba29f83..01e0275feeb 100644 --- a/homeassistant/components/seven_segments/manifest.json +++ b/homeassistant/components/seven_segments/manifest.json @@ -2,6 +2,6 @@ "domain": "seven_segments", "name": "Seven Segments OCR", "documentation": "https://www.home-assistant.io/integrations/seven_segments", - "requirements": ["pillow==7.2.0"], + "requirements": ["pillow==8.1.0"], "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/sharkiq/translations/de.json b/homeassistant/components/sharkiq/translations/de.json index 2294960d6f2..8a6f9b14747 100644 --- a/homeassistant/components/sharkiq/translations/de.json +++ b/homeassistant/components/sharkiq/translations/de.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "cannot_connect": "Verbindungsfehler", + "already_configured": "Konto wurde bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", "unknown": "Unerwarteter Fehler" }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/sharkiq/translations/fr.json b/homeassistant/components/sharkiq/translations/fr.json index 5f05292ec2a..6fa3ba7707c 100644 --- a/homeassistant/components/sharkiq/translations/fr.json +++ b/homeassistant/components/sharkiq/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9", + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", "cannot_connect": "\u00c9chec de connexion", "reauth_successful": "Jeton d'acc\u00e8s mis \u00e0 jour avec succ\u00e8s", "unknown": "Erreur inattendue" diff --git a/homeassistant/components/sharkiq/translations/tr.json b/homeassistant/components/sharkiq/translations/tr.json new file mode 100644 index 00000000000..c82f1e8bf05 --- /dev/null +++ b/homeassistant/components/sharkiq/translations/tr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "unknown": "Beklenmeyen hata" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "reauth": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + }, + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sharkiq/translations/uk.json b/homeassistant/components/sharkiq/translations/uk.json new file mode 100644 index 00000000000..0f78c62fa7e --- /dev/null +++ b/homeassistant/components/sharkiq/translations/uk.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "reauth": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/__init__.py b/homeassistant/components/shelly/__init__.py index 147d9fb950d..d2df03b44a5 100644 --- a/homeassistant/components/shelly/__init__.py +++ b/homeassistant/components/shelly/__init__.py @@ -9,6 +9,7 @@ import async_timeout from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( + ATTR_DEVICE_ID, CONF_HOST, CONF_PASSWORD, CONF_USERNAME, @@ -24,15 +25,19 @@ from homeassistant.helpers import ( ) from .const import ( + AIOSHELLY_DEVICE_TIMEOUT_SEC, + ATTR_CHANNEL, + ATTR_CLICK_TYPE, + ATTR_DEVICE, BATTERY_DEVICES_WITH_PERMANENT_CONNECTION, COAP, DATA_CONFIG_ENTRY, DOMAIN, + EVENT_SHELLY_CLICK, INPUTS_EVENTS_DICT, POLLING_TIMEOUT_MULTIPLIER, REST, REST_SENSORS_UPDATE_INTERVAL, - SETUP_ENTRY_TIMEOUT_SEC, SLEEP_PERIOD_MULTIPLIER, UPDATE_PERIOD_MULTIPLIER, ) @@ -79,7 +84,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): coap_context = await get_coap_context(hass) try: - async with async_timeout.timeout(SETUP_ENTRY_TIMEOUT_SEC): + async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): device = await aioshelly.Device.create( aiohttp_client.async_get_clientsession(hass), coap_context, @@ -170,12 +175,12 @@ class ShellyDeviceWrapper(update_coordinator.DataUpdateCoordinator): if event_type in INPUTS_EVENTS_DICT: self.hass.bus.async_fire( - "shelly.click", + EVENT_SHELLY_CLICK, { - "device_id": self.device_id, - "device": self.device.settings["device"]["hostname"], - "channel": channel, - "click_type": INPUTS_EVENTS_DICT[event_type], + ATTR_DEVICE_ID: self.device_id, + ATTR_DEVICE: self.device.settings["device"]["hostname"], + ATTR_CHANNEL: channel, + ATTR_CLICK_TYPE: INPUTS_EVENTS_DICT[event_type], }, ) else: @@ -263,7 +268,7 @@ class ShellyDeviceRestWrapper(update_coordinator.DataUpdateCoordinator): async def _async_update_data(self): """Fetch data.""" try: - async with async_timeout.timeout(5): + async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): _LOGGER.debug("REST update for %s", self.name) return await self.device.update_status() except OSError as err: diff --git a/homeassistant/components/shelly/config_flow.py b/homeassistant/components/shelly/config_flow.py index b3dd7bb80fe..b47c76cbb7a 100644 --- a/homeassistant/components/shelly/config_flow.py +++ b/homeassistant/components/shelly/config_flow.py @@ -18,6 +18,7 @@ from homeassistant.const import ( from homeassistant.helpers import aiohttp_client from . import get_coap_context +from .const import AIOSHELLY_DEVICE_TIMEOUT_SEC from .const import DOMAIN # pylint:disable=unused-import _LOGGER = logging.getLogger(__name__) @@ -39,7 +40,7 @@ async def validate_input(hass: core.HomeAssistant, host, data): ) coap_context = await get_coap_context(hass) - async with async_timeout.timeout(5): + async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): device = await aioshelly.Device.create( aiohttp_client.async_get_clientsession(hass), coap_context, @@ -187,7 +188,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_get_info(self, host): """Get info from shelly device.""" - async with async_timeout.timeout(5): + async with async_timeout.timeout(AIOSHELLY_DEVICE_TIMEOUT_SEC): return await aioshelly.get_info( aiohttp_client.async_get_clientsession(self.hass), host, diff --git a/homeassistant/components/shelly/const.py b/homeassistant/components/shelly/const.py index 9f5c5b2efc7..a5922d0b9c0 100644 --- a/homeassistant/components/shelly/const.py +++ b/homeassistant/components/shelly/const.py @@ -11,8 +11,8 @@ POLLING_TIMEOUT_MULTIPLIER = 1.2 # Refresh interval for REST sensors REST_SENSORS_UPDATE_INTERVAL = 60 -# Timeout used for initial entry setup in "async_setup_entry". -SETUP_ENTRY_TIMEOUT_SEC = 10 +# Timeout used for aioshelly calls +AIOSHELLY_DEVICE_TIMEOUT_SEC = 10 # Multiplier used to calculate the "update_interval" for sleeping devices. SLEEP_PERIOD_MULTIPLIER = 1.2 @@ -35,3 +35,43 @@ INPUTS_EVENTS_DICT = { # List of battery devices that maintain a permanent WiFi connection BATTERY_DEVICES_WITH_PERMANENT_CONNECTION = ["SHMOS-01"] + +EVENT_SHELLY_CLICK = "shelly.click" + +ATTR_CLICK_TYPE = "click_type" +ATTR_CHANNEL = "channel" +ATTR_DEVICE = "device" +CONF_SUBTYPE = "subtype" + +BASIC_INPUTS_EVENTS_TYPES = { + "single", + "long", +} + +SHBTN_1_INPUTS_EVENTS_TYPES = { + "single", + "double", + "triple", + "long", +} + +SUPPORTED_INPUTS_EVENTS_TYPES = SHIX3_1_INPUTS_EVENTS_TYPES = { + "single", + "double", + "triple", + "long", + "single_long", + "long_single", +} + +INPUTS_EVENTS_SUBTYPES = { + "button": 1, + "button1": 1, + "button2": 2, + "button3": 3, +} + +# Kelvin value for colorTemp +KELVIN_MAX_VALUE = 6500 +KELVIN_MIN_VALUE = 2700 +KELVIN_MIN_VALUE_SHBLB_1 = 3000 diff --git a/homeassistant/components/shelly/device_trigger.py b/homeassistant/components/shelly/device_trigger.py new file mode 100644 index 00000000000..f6cdfaee19f --- /dev/null +++ b/homeassistant/components/shelly/device_trigger.py @@ -0,0 +1,110 @@ +"""Provides device triggers for Shelly.""" +from typing import List + +import voluptuous as vol + +from homeassistant.components.automation import AutomationActionType +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) +from homeassistant.components.homeassistant.triggers import event as event_trigger +from homeassistant.const import ( + ATTR_DEVICE_ID, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_EVENT, + CONF_PLATFORM, + CONF_TYPE, +) +from homeassistant.core import CALLBACK_TYPE, HomeAssistant +from homeassistant.helpers.typing import ConfigType + +from .const import ( + ATTR_CHANNEL, + ATTR_CLICK_TYPE, + CONF_SUBTYPE, + DOMAIN, + EVENT_SHELLY_CLICK, + INPUTS_EVENTS_SUBTYPES, + SUPPORTED_INPUTS_EVENTS_TYPES, +) +from .utils import get_device_wrapper, get_input_triggers + +TRIGGER_SCHEMA = TRIGGER_BASE_SCHEMA.extend( + { + vol.Required(CONF_TYPE): vol.In(SUPPORTED_INPUTS_EVENTS_TYPES), + vol.Required(CONF_SUBTYPE): vol.In(INPUTS_EVENTS_SUBTYPES), + } +) + + +async def async_validate_trigger_config(hass, config): + """Validate config.""" + config = TRIGGER_SCHEMA(config) + + # if device is available verify parameters against device capabilities + wrapper = get_device_wrapper(hass, config[CONF_DEVICE_ID]) + if not wrapper: + return config + + trigger = (config[CONF_TYPE], config[CONF_SUBTYPE]) + + for block in wrapper.device.blocks: + input_triggers = get_input_triggers(wrapper.device, block) + if trigger in input_triggers: + return config + + raise InvalidDeviceAutomationConfig( + f"Invalid ({CONF_TYPE},{CONF_SUBTYPE}): {trigger}" + ) + + +async def async_get_triggers(hass: HomeAssistant, device_id: str) -> List[dict]: + """List device triggers for Shelly devices.""" + triggers = [] + + wrapper = get_device_wrapper(hass, device_id) + if not wrapper: + raise InvalidDeviceAutomationConfig(f"Device not found: {device_id}") + + for block in wrapper.device.blocks: + input_triggers = get_input_triggers(wrapper.device, block) + + for trigger, subtype in input_triggers: + triggers.append( + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: trigger, + CONF_SUBTYPE: subtype, + } + ) + + return triggers + + +async def async_attach_trigger( + hass: HomeAssistant, + config: ConfigType, + action: AutomationActionType, + automation_info: dict, +) -> CALLBACK_TYPE: + """Attach a trigger.""" + config = TRIGGER_SCHEMA(config) + event_config = event_trigger.TRIGGER_SCHEMA( + { + event_trigger.CONF_PLATFORM: CONF_EVENT, + event_trigger.CONF_EVENT_TYPE: EVENT_SHELLY_CLICK, + event_trigger.CONF_EVENT_DATA: { + ATTR_DEVICE_ID: config[CONF_DEVICE_ID], + ATTR_CHANNEL: INPUTS_EVENTS_SUBTYPES[config[CONF_SUBTYPE]], + ATTR_CLICK_TYPE: config[CONF_TYPE], + }, + } + ) + event_config = event_trigger.TRIGGER_SCHEMA(event_config) + return await event_trigger.async_attach_trigger( + hass, event_config, action, automation_info, platform_type="device" + ) diff --git a/homeassistant/components/shelly/light.py b/homeassistant/components/shelly/light.py index b3a6869d67d..0c91ddc1088 100644 --- a/homeassistant/components/shelly/light.py +++ b/homeassistant/components/shelly/light.py @@ -1,27 +1,47 @@ """Light for Shelly.""" -from typing import Optional +from typing import Optional, Tuple from aioshelly import Block from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + ATTR_WHITE_VALUE, SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, SUPPORT_COLOR_TEMP, + SUPPORT_WHITE_VALUE, LightEntity, ) from homeassistant.core import callback from homeassistant.util.color import ( + color_hs_to_RGB, + color_RGB_to_hs, color_temperature_kelvin_to_mired, color_temperature_mired_to_kelvin, ) from . import ShellyDeviceWrapper -from .const import COAP, DATA_CONFIG_ENTRY, DOMAIN +from .const import ( + COAP, + DATA_CONFIG_ENTRY, + DOMAIN, + KELVIN_MAX_VALUE, + KELVIN_MIN_VALUE, + KELVIN_MIN_VALUE_SHBLB_1, +) from .entity import ShellyBlockEntity from .utils import async_remove_shelly_entity +def min_kelvin(model: str): + """Kelvin (min) for colorTemp.""" + if model in ["SHBLB-1"]: + return KELVIN_MIN_VALUE_SHBLB_1 + return KELVIN_MIN_VALUE + + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up lights for device.""" wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][COAP] @@ -54,11 +74,17 @@ class ShellyLight(ShellyBlockEntity, LightEntity): """Initialize light.""" super().__init__(wrapper, block) self.control_result = None + self.mode_result = None self._supported_features = 0 - if hasattr(block, "brightness"): + + if hasattr(block, "brightness") or hasattr(block, "gain"): self._supported_features |= SUPPORT_BRIGHTNESS if hasattr(block, "colorTemp"): self._supported_features |= SUPPORT_COLOR_TEMP + if hasattr(block, "white"): + self._supported_features |= SUPPORT_WHITE_VALUE + if hasattr(block, "red") and hasattr(block, "green") and hasattr(block, "blue"): + self._supported_features |= SUPPORT_COLOR @property def supported_features(self) -> int: @@ -73,18 +99,70 @@ class ShellyLight(ShellyBlockEntity, LightEntity): return self.block.output + @property + def mode(self) -> Optional[str]: + """Return the color mode of the light.""" + if self.mode_result: + return self.mode_result["mode"] + + if hasattr(self.block, "mode"): + return self.block.mode + + if ( + hasattr(self.block, "red") + and hasattr(self.block, "green") + and hasattr(self.block, "blue") + ): + return "color" + + return "white" + @property def brightness(self) -> Optional[int]: """Brightness of light.""" - if self.control_result: - brightness = self.control_result["brightness"] + if self.mode == "color": + if self.control_result: + brightness = self.control_result["gain"] + else: + brightness = self.block.gain else: - brightness = self.block.brightness + if self.control_result: + brightness = self.control_result["brightness"] + else: + brightness = self.block.brightness return int(brightness / 100 * 255) + @property + def white_value(self) -> Optional[int]: + """White value of light.""" + if self.control_result: + white = self.control_result["white"] + else: + white = self.block.white + return int(white) + + @property + def hs_color(self) -> Optional[Tuple[float, float]]: + """Return the hue and saturation color value of light.""" + if self.mode == "white": + return color_RGB_to_hs(255, 255, 255) + + if self.control_result: + red = self.control_result["red"] + green = self.control_result["green"] + blue = self.control_result["blue"] + else: + red = self.block.red + green = self.block.green + blue = self.block.blue + return color_RGB_to_hs(red, green, blue) + @property def color_temp(self) -> Optional[float]: """Return the CT color value in mireds.""" + if self.mode == "color": + return None + if self.control_result: color_temp = self.control_result["temp"] else: @@ -93,33 +171,52 @@ class ShellyLight(ShellyBlockEntity, LightEntity): # If you set DUO to max mireds in Shelly app, 2700K, # It reports 0 temp if color_temp == 0: - return self.max_mireds + return min_kelvin(self.wrapper.model) return int(color_temperature_kelvin_to_mired(color_temp)) @property - def min_mireds(self) -> float: + def min_mireds(self) -> Optional[float]: """Return the coldest color_temp that this light supports.""" - return color_temperature_kelvin_to_mired(6500) + return color_temperature_kelvin_to_mired(KELVIN_MAX_VALUE) @property - def max_mireds(self) -> float: + def max_mireds(self) -> Optional[float]: """Return the warmest color_temp that this light supports.""" - return color_temperature_kelvin_to_mired(2700) + return color_temperature_kelvin_to_mired(min_kelvin(self.wrapper.model)) async def async_turn_on(self, **kwargs) -> None: """Turn on light.""" params = {"turn": "on"} if ATTR_BRIGHTNESS in kwargs: - tmp_brightness = kwargs[ATTR_BRIGHTNESS] - params["brightness"] = int(tmp_brightness / 255 * 100) + tmp_brightness = int(kwargs[ATTR_BRIGHTNESS] / 255 * 100) + if hasattr(self.block, "gain"): + params["gain"] = tmp_brightness + if hasattr(self.block, "brightness"): + params["brightness"] = tmp_brightness if ATTR_COLOR_TEMP in kwargs: color_temp = color_temperature_mired_to_kelvin(kwargs[ATTR_COLOR_TEMP]) - if color_temp > 6500: - color_temp = 6500 - elif color_temp < 2700: - color_temp = 2700 + color_temp = min( + KELVIN_MAX_VALUE, max(min_kelvin(self.wrapper.model), color_temp) + ) + # Color temperature change - used only in white mode, switch device mode to white + if self.mode == "color": + self.mode_result = await self.wrapper.device.switch_light_mode("white") + params["red"] = params["green"] = params["blue"] = 255 params["temp"] = int(color_temp) + elif ATTR_HS_COLOR in kwargs: + red, green, blue = color_hs_to_RGB(*kwargs[ATTR_HS_COLOR]) + # Color channels change - used only in color mode, switch device mode to color + if self.mode == "white": + self.mode_result = await self.wrapper.device.switch_light_mode("color") + params["red"] = red + params["green"] = green + params["blue"] = blue + elif ATTR_WHITE_VALUE in kwargs: + # White channel change - used only in color mode, switch device mode device to color + if self.mode == "white": + self.mode_result = await self.wrapper.device.switch_light_mode("color") + params["white"] = int(kwargs[ATTR_WHITE_VALUE]) self.control_result = await self.block.set_state(**params) self.async_write_ha_state() @@ -130,6 +227,7 @@ class ShellyLight(ShellyBlockEntity, LightEntity): @callback def _update_callback(self): - """When device updates, clear control result that overrides state.""" + """When device updates, clear control & mode result that overrides state.""" self.control_result = None + self.mode_result = None super()._update_callback() diff --git a/homeassistant/components/shelly/logbook.py b/homeassistant/components/shelly/logbook.py new file mode 100644 index 00000000000..78a5c279a93 --- /dev/null +++ b/homeassistant/components/shelly/logbook.py @@ -0,0 +1,37 @@ +"""Describe Shelly logbook events.""" + +from homeassistant.const import ATTR_DEVICE_ID +from homeassistant.core import callback + +from .const import ( + ATTR_CHANNEL, + ATTR_CLICK_TYPE, + ATTR_DEVICE, + DOMAIN, + EVENT_SHELLY_CLICK, +) +from .utils import get_device_name, get_device_wrapper + + +@callback +def async_describe_events(hass, async_describe_event): + """Describe logbook events.""" + + @callback + def async_describe_shelly_click_event(event): + """Describe shelly.click logbook event.""" + wrapper = get_device_wrapper(hass, event.data[ATTR_DEVICE_ID]) + if wrapper: + device_name = get_device_name(wrapper.device) + else: + device_name = event.data[ATTR_DEVICE] + + channel = event.data[ATTR_CHANNEL] + click_type = event.data[ATTR_CLICK_TYPE] + + return { + "name": "Shelly", + "message": f"'{click_type}' click event for {device_name} channel {channel} was fired.", + } + + async_describe_event(DOMAIN, EVENT_SHELLY_CLICK, async_describe_shelly_click_event) diff --git a/homeassistant/components/shelly/manifest.json b/homeassistant/components/shelly/manifest.json index 00f9620b3c5..923bcdced34 100644 --- a/homeassistant/components/shelly/manifest.json +++ b/homeassistant/components/shelly/manifest.json @@ -3,7 +3,7 @@ "name": "Shelly", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/shelly", - "requirements": ["aioshelly==0.5.1"], + "requirements": ["aioshelly==0.5.3"], "zeroconf": [{ "type": "_http._tcp.local.", "name": "shelly*" }], - "codeowners": ["@balloob", "@bieniu", "@thecode"] + "codeowners": ["@balloob", "@bieniu", "@thecode", "@chemelli74"] } diff --git a/homeassistant/components/shelly/strings.json b/homeassistant/components/shelly/strings.json index 6a1cbbd5797..341328801cc 100644 --- a/homeassistant/components/shelly/strings.json +++ b/homeassistant/components/shelly/strings.json @@ -27,5 +27,21 @@ "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", "unsupported_firmware": "The device is using an unsupported firmware version." } + }, + "device_automation":{ + "trigger_subtype": { + "button": "Button", + "button1": "First button", + "button2": "Second button", + "button3": "Third button" + }, + "trigger_type": { + "single": "{subtype} single clicked", + "double": "{subtype} double clicked", + "triple": "{subtype} triple clicked", + "long": " {subtype} long clicked", + "single_long": "{subtype} single clicked and then long clicked", + "long_single": "{subtype} long clicked and then single clicked" + } } } diff --git a/homeassistant/components/shelly/translations/ca.json b/homeassistant/components/shelly/translations/ca.json index 2bf17c2ba77..c2df82c0b16 100644 --- a/homeassistant/components/shelly/translations/ca.json +++ b/homeassistant/components/shelly/translations/ca.json @@ -27,5 +27,21 @@ "description": "Abans de configurar-lo, els dispositius amb bateria s'han de desperar prement el bot\u00f3 del dispositiu." } } + }, + "device_automation": { + "trigger_subtype": { + "button": "Bot\u00f3", + "button1": "Primer bot\u00f3", + "button2": "Segon bot\u00f3", + "button3": "Tercer bot\u00f3" + }, + "trigger_type": { + "double": "{subtype} clicat dues vegades", + "long": "{subtype} clicat durant una estona", + "long_single": "{subtype} clicat durant una estona i despr\u00e9s r\u00e0pid", + "single": "{subtype} clicat una vegada", + "single_long": "{subtype} clicat r\u00e0pid i, despr\u00e9s, durant una estona", + "triple": "{subtype} clicat tres vegades" + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/cs.json b/homeassistant/components/shelly/translations/cs.json index 41ca338ab9e..afdfe7c8f56 100644 --- a/homeassistant/components/shelly/translations/cs.json +++ b/homeassistant/components/shelly/translations/cs.json @@ -27,5 +27,21 @@ "description": "P\u0159ed nastaven\u00edm mus\u00ed b\u00fdt za\u0159\u00edzen\u00ed nap\u00e1jen\u00e9 z baterie probuzeno stisknut\u00edm tla\u010d\u00edtka na dan\u00e9m za\u0159\u00edzen\u00ed." } } + }, + "device_automation": { + "trigger_subtype": { + "button": "Tla\u010d\u00edtko", + "button1": "Prvn\u00ed tla\u010d\u00edtko", + "button2": "Druh\u00e9 tla\u010d\u00edtko", + "button3": "T\u0159et\u00ed tla\u010d\u00edtko" + }, + "trigger_type": { + "double": "\"{subtype}\" stisknuto dvakr\u00e1t", + "long": "\"{subtype}\" stisknuto dlouze", + "long_single": "\"{subtype}\" stisknuto dlouze a pak jednou", + "single": "\"{subtype}\" stisknuto jednou", + "single_long": "\"{subtype}\" stisknuto jednou a pak dlouze", + "triple": "\"{subtype}\" stisknuto t\u0159ikr\u00e1t" + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/da.json b/homeassistant/components/shelly/translations/da.json new file mode 100644 index 00000000000..08631bc39e1 --- /dev/null +++ b/homeassistant/components/shelly/translations/da.json @@ -0,0 +1,17 @@ +{ + "device_automation": { + "trigger_subtype": { + "button": "Knap", + "button1": "F\u00f8rste knap", + "button2": "Anden knap", + "button3": "Tredje knap" + }, + "trigger_type": { + "double": "{subtype} dobbelt klik", + "long": "{subtype} langt klik", + "long_single": "{subtype} langt klik og derefter enkelt klik", + "single": "{subtype} enkelt klik", + "single_long": "{subtype} enkelt klik og derefter langt klik" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/de.json b/homeassistant/components/shelly/translations/de.json index 74d0f831c8b..4764936a41b 100644 --- a/homeassistant/components/shelly/translations/de.json +++ b/homeassistant/components/shelly/translations/de.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "flow_title": "Shelly: {name}", @@ -15,7 +19,8 @@ "user": { "data": { "host": "Host" - } + }, + "description": "Vor der Einrichtung m\u00fcssen batteriebetriebene Ger\u00e4te durch Dr\u00fccken der Taste am Ger\u00e4t aufgeweckt werden." } } } diff --git a/homeassistant/components/shelly/translations/en.json b/homeassistant/components/shelly/translations/en.json index edb6da27a99..a9ad6092a08 100644 --- a/homeassistant/components/shelly/translations/en.json +++ b/homeassistant/components/shelly/translations/en.json @@ -27,5 +27,21 @@ "description": "Before set up, battery-powered devices must be woken up by pressing the button on the device." } } + }, + "device_automation": { + "trigger_subtype": { + "button": "Button", + "button1": "First button", + "button2": "Second button", + "button3": "Third button" + }, + "trigger_type": { + "double": "{subtype} double clicked", + "long": " {subtype} long clicked", + "long_single": "{subtype} long clicked and then single clicked", + "single": "{subtype} single clicked", + "single_long": "{subtype} single clicked and then long clicked", + "triple": "{subtype} triple clicked" + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/es.json b/homeassistant/components/shelly/translations/es.json index 38c72f21dca..09cc3f51378 100644 --- a/homeassistant/components/shelly/translations/es.json +++ b/homeassistant/components/shelly/translations/es.json @@ -27,5 +27,21 @@ "description": "Antes de configurarlo, el dispositivo que funciona con bater\u00eda debe despertarse presionando el bot\u00f3n del dispositivo." } } + }, + "device_automation": { + "trigger_subtype": { + "button": "Bot\u00f3n", + "button1": "Primer bot\u00f3n", + "button2": "Segundo bot\u00f3n", + "button3": "Tercer bot\u00f3n" + }, + "trigger_type": { + "double": "Pulsaci\u00f3n doble de {subtype}", + "long": "Pulsaci\u00f3n larga de {subtype}", + "long_single": "Pulsaci\u00f3n larga de {subtype} seguida de una pulsaci\u00f3n simple", + "single": "Pulsaci\u00f3n simple de {subtype}", + "single_long": "Pulsaci\u00f3n simple de {subtype} seguida de una pulsaci\u00f3n larga", + "triple": "Pulsaci\u00f3n triple de {subtype}" + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/et.json b/homeassistant/components/shelly/translations/et.json index 12a662f6560..d2514876a81 100644 --- a/homeassistant/components/shelly/translations/et.json +++ b/homeassistant/components/shelly/translations/et.json @@ -27,5 +27,21 @@ "description": "Enne seadistamist tuleb akutoitega seade \u00e4ratada vajutades seadme nuppu." } } + }, + "device_automation": { + "trigger_subtype": { + "button": "Nupp", + "button1": "Esimene nupp", + "button2": "Teine nupp", + "button3": "Kolmas nupp" + }, + "trigger_type": { + "double": "Nuppu {subtype} topeltkl\u00f5psati", + "long": "Nuppu \"{subtype}\" hoiti all", + "long_single": "Nuppu {subtype} hoiti all ja seej\u00e4rel kl\u00f5psati", + "single": "Nuppu {subtype} kl\u00f5psati", + "single_long": "Nuppu {subtype} kl\u00f5psati \u00fcks kord ja seej\u00e4rel hoiti all", + "triple": "Nuppu {subtype} kl\u00f5psati kolm korda" + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/it.json b/homeassistant/components/shelly/translations/it.json index 61f2f8ccd09..4d486a8f2fa 100644 --- a/homeassistant/components/shelly/translations/it.json +++ b/homeassistant/components/shelly/translations/it.json @@ -27,5 +27,21 @@ "description": "Prima della configurazione, i dispositivi alimentati a batteria devono essere riattivati premendo il pulsante sul dispositivo." } } + }, + "device_automation": { + "trigger_subtype": { + "button": "Pulsante", + "button1": "Primo pulsante", + "button2": "Secondo pulsante", + "button3": "Terzo pulsante" + }, + "trigger_type": { + "double": "{subtype} premuto due volte", + "long": "{subtype} premuto a lungo", + "long_single": "{subtype} premuto a lungo e poi singolarmente", + "single": "{subtype} premuto singolarmente", + "single_long": "{subtype} premuto singolarmente e poi a lungo", + "triple": "{subtype} premuto tre volte" + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/lb.json b/homeassistant/components/shelly/translations/lb.json index b358c1c7282..e6c5d8330c6 100644 --- a/homeassistant/components/shelly/translations/lb.json +++ b/homeassistant/components/shelly/translations/lb.json @@ -27,5 +27,13 @@ "description": "Virum ariichten muss dat Batterie bedriwwen Ger\u00e4t aktiv\u00e9iert ginn andeems de Kn\u00e4ppchen um Apparat gedr\u00e9ckt g\u00ebtt." } } + }, + "device_automation": { + "trigger_subtype": { + "button": "Kn\u00e4ppchen", + "button1": "\u00c9ischte Kn\u00e4ppchen", + "button2": "Zweete Kn\u00e4ppchen", + "button3": "Dr\u00ebtte Kn\u00e4ppchen" + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/no.json b/homeassistant/components/shelly/translations/no.json index 705c494a4c1..1606a1acbb1 100644 --- a/homeassistant/components/shelly/translations/no.json +++ b/homeassistant/components/shelly/translations/no.json @@ -27,5 +27,21 @@ "description": "F\u00f8r du setter opp, m\u00e5 batteridrevne enheter vekkes ved \u00e5 trykke p\u00e5 knappen p\u00e5 enheten." } } + }, + "device_automation": { + "trigger_subtype": { + "button": "Knapp", + "button1": "F\u00f8rste knapp", + "button2": "Andre knapp", + "button3": "Tredje knapp" + }, + "trigger_type": { + "double": "{subtype} dobbeltklikket", + "long": "{subtype} lenge klikket", + "long_single": "{subtype} lengre klikk og deretter et enkeltklikk", + "single": "{subtype} enkeltklikket", + "single_long": "{subtype} enkeltklikket og deretter et lengre klikk", + "triple": "{subtype} trippelklikket" + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/pl.json b/homeassistant/components/shelly/translations/pl.json index ebf6041d4ba..cd8ffac7138 100644 --- a/homeassistant/components/shelly/translations/pl.json +++ b/homeassistant/components/shelly/translations/pl.json @@ -27,5 +27,21 @@ "description": "Przed skonfigurowaniem urz\u0105dzenia zasilane bateryjnie nale\u017cy, wybudzi\u0107 naciskaj\u0105c przycisk na urz\u0105dzeniu." } } + }, + "device_automation": { + "trigger_subtype": { + "button": "Przycisk", + "button1": "pierwszy", + "button2": "drugi", + "button3": "trzeci" + }, + "trigger_type": { + "double": "przycisk \"{subtype}\" zostanie dwukrotnie naci\u015bni\u0119ty", + "long": "przycisk \"{subtype}\" zostanie d\u0142ugo naci\u015bni\u0119ty", + "long_single": "przycisk \"{subtype}\" zostanie d\u0142ugo naci\u015bni\u0119ty, a nast\u0119pnie pojedynczo naci\u015bni\u0119ty", + "single": "przycisk \"{subtype}\" zostanie pojedynczo naci\u015bni\u0119ty", + "single_long": "przycisk \"{subtype}\" pojedynczo naci\u015bni\u0119ty, a nast\u0119pnie d\u0142ugo naci\u015bni\u0119ty", + "triple": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty" + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/ru.json b/homeassistant/components/shelly/translations/ru.json index 508a189b849..5a3a40ac9f8 100644 --- a/homeassistant/components/shelly/translations/ru.json +++ b/homeassistant/components/shelly/translations/ru.json @@ -27,5 +27,21 @@ "description": "\u041f\u0435\u0440\u0435\u0434 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u043e\u0439 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430, \u0440\u0430\u0431\u043e\u0442\u0430\u044e\u0449\u0438\u0435 \u043e\u0442 \u0431\u0430\u0442\u0430\u0440\u0435\u0438, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u0432\u0435\u0441\u0442\u0438 \u0438\u0437 \u0441\u043f\u044f\u0449\u0435\u0433\u043e \u0440\u0435\u0436\u0438\u043c\u0430, \u043d\u0430\u0436\u0430\u0432 \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0435." } } + }, + "device_automation": { + "trigger_subtype": { + "button": "\u041a\u043d\u043e\u043f\u043a\u0430", + "button1": "\u041f\u0435\u0440\u0432\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button2": "\u0412\u0442\u043e\u0440\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button3": "\u0422\u0440\u0435\u0442\u044c\u044f \u043a\u043d\u043e\u043f\u043a\u0430" + }, + "trigger_type": { + "double": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0430", + "long": "{subtype} \u0434\u043e\u043b\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0430", + "long_single": "{subtype} \u0434\u043e\u043b\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0430 \u0438 \u0437\u0430\u0442\u0435\u043c \u043d\u0430\u0436\u0430\u0442\u0430 \u043e\u0434\u0438\u043d \u0440\u0430\u0437", + "single": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u043e\u0434\u0438\u043d \u0440\u0430\u0437", + "single_long": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u043e\u0434\u0438\u043d \u0440\u0430\u0437 \u0438 \u0437\u0430\u0442\u0435\u043c \u0434\u043e\u043b\u0433\u043e \u043d\u0430\u0436\u0430\u0442\u0430", + "triple": "{subtype} \u043d\u0430\u0436\u0430\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0430" + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/tr.json b/homeassistant/components/shelly/translations/tr.json new file mode 100644 index 00000000000..f577c73787f --- /dev/null +++ b/homeassistant/components/shelly/translations/tr.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "flow_title": "{name}", + "step": { + "credentials": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + }, + "user": { + "data": { + "host": "Ana Bilgisayar" + } + } + } + }, + "device_automation": { + "trigger_subtype": { + "button": "D\u00fc\u011fme", + "button1": "\u0130lk d\u00fc\u011fme", + "button2": "\u0130kinci d\u00fc\u011fme", + "button3": "\u00dc\u00e7\u00fcnc\u00fc d\u00fc\u011fme" + }, + "trigger_type": { + "double": "{subtype} \u00e7ift t\u0131kland\u0131", + "long": "{subtype} uzun t\u0131kland\u0131", + "long_single": "{subtype} uzun t\u0131kland\u0131 ve ard\u0131ndan tek t\u0131kland\u0131", + "single": "{subtype} tek t\u0131kland\u0131", + "triple": "{subtype} \u00fc\u00e7 kez t\u0131kland\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/uk.json b/homeassistant/components/shelly/translations/uk.json new file mode 100644 index 00000000000..7ad70b0f0da --- /dev/null +++ b/homeassistant/components/shelly/translations/uk.json @@ -0,0 +1,47 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "unsupported_firmware": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454 \u043d\u0435\u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0443 \u0432\u0435\u0440\u0441\u0456\u044e \u043c\u0456\u043a\u0440\u043e\u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0438." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "flow_title": "{name}", + "step": { + "confirm_discovery": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 {model} ({host})? \n\n\u041f\u0435\u0440\u0435\u0434 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457, \u0449\u043e \u043f\u0440\u0430\u0446\u044e\u044e\u0442\u044c \u0432\u0456\u0434 \u0431\u0430\u0442\u0430\u0440\u0435\u0457, \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u0432\u0438\u0432\u0435\u0441\u0442\u0438 \u0437\u0456 \u0441\u043f\u043b\u044f\u0447\u043e\u0433\u043e \u0440\u0435\u0436\u0438\u043c\u0443, \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0432\u0448\u0438 \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457." + }, + "credentials": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u041f\u0435\u0440\u0435\u0434 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457, \u0449\u043e \u043f\u0440\u0430\u0446\u044e\u044e\u0442\u044c \u0432\u0456\u0434 \u0431\u0430\u0442\u0430\u0440\u0435\u0457, \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u0432\u0438\u0432\u0435\u0441\u0442\u0438 \u0437\u0456 \u0441\u043f\u043b\u044f\u0447\u043e\u0433\u043e \u0440\u0435\u0436\u0438\u043c\u0443, \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0432\u0448\u0438 \u043a\u043d\u043e\u043f\u043a\u0443 \u043d\u0430 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457." + } + } + }, + "device_automation": { + "trigger_subtype": { + "button": "\u041a\u043d\u043e\u043f\u043a\u0430", + "button1": "\u041f\u0435\u0440\u0448\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "button2": "\u0414\u0440\u0443\u0433\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "button3": "\u0422\u0440\u0435\u0442\u044f \u043a\u043d\u043e\u043f\u043a\u0430" + }, + "trigger_type": { + "double": "{subtype} \u043f\u043e\u0434\u0432\u0456\u0439\u043d\u0438\u0439 \u043a\u043b\u0456\u043a", + "long": "{subtype} \u0434\u043e\u0432\u0433\u0438\u0439 \u043a\u043b\u0456\u043a", + "long_single": "{subtype} \u0434\u043e\u0432\u0433\u0438\u0439 \u043a\u043b\u0456\u043a, \u0430 \u043f\u043e\u0442\u0456\u043c \u043e\u0434\u0438\u043d \u043a\u043b\u0456\u043a", + "single": "{subtype} \u043e\u0434\u0438\u043d\u0430\u0440\u043d\u0438\u0439 \u043a\u043b\u0456\u043a", + "single_long": "{subtype} \u043e\u0434\u0438\u043d\u0430\u0440\u043d\u0438\u0439 \u043a\u043b\u0456\u043a, \u043f\u043e\u0442\u0456\u043c \u0434\u043e\u0432\u0433\u0438\u0439 \u043a\u043b\u0456\u043a", + "triple": "{subtype} \u043f\u043e\u0442\u0440\u0456\u0439\u043d\u0438\u0439 \u043a\u043b\u0456\u043a" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shelly/translations/zh-Hant.json b/homeassistant/components/shelly/translations/zh-Hant.json index bf0150523b3..8f315208135 100644 --- a/homeassistant/components/shelly/translations/zh-Hant.json +++ b/homeassistant/components/shelly/translations/zh-Hant.json @@ -27,5 +27,21 @@ "description": "\u958b\u59cb\u8a2d\u5b9a\u524d\uff0c\u5fc5\u9808\u6309\u4e0b\u88dd\u7f6e\u4e0a\u7684\u6309\u9215\u4ee5\u559a\u9192\u96fb\u6c60\u4f9b\u96fb\u88dd\u7f6e\u3002" } } + }, + "device_automation": { + "trigger_subtype": { + "button": "\u6309\u9215", + "button1": "\u7b2c\u4e00\u500b\u6309\u9215", + "button2": "\u7b2c\u4e8c\u500b\u6309\u9215", + "button3": "\u7b2c\u4e09\u500b\u6309\u9215" + }, + "trigger_type": { + "double": "{subtype} \u96d9\u64ca", + "long": "{subtype} \u9577\u6309", + "long_single": "{subtype} \u9577\u6309\u5f8c\u55ae\u64ca", + "single": "{subtype} \u55ae\u64ca", + "single_long": "{subtype} \u55ae\u64ca\u5f8c\u9577\u6309", + "triple": "{subtype} \u4e09\u9023\u64ca" + } } } \ No newline at end of file diff --git a/homeassistant/components/shelly/utils.py b/homeassistant/components/shelly/utils.py index 976afdd755b..97d8bda609b 100644 --- a/homeassistant/components/shelly/utils.py +++ b/homeassistant/components/shelly/utils.py @@ -2,14 +2,22 @@ from datetime import timedelta import logging -from typing import Optional +from typing import List, Optional, Tuple import aioshelly from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import HomeAssistant from homeassistant.util.dt import parse_datetime, utcnow -from .const import DOMAIN +from .const import ( + BASIC_INPUTS_EVENTS_TYPES, + COAP, + DATA_CONFIG_ENTRY, + DOMAIN, + SHBTN_1_INPUTS_EVENTS_TYPES, + SHIX3_1_INPUTS_EVENTS_TYPES, +) _LOGGER = logging.getLogger(__name__) @@ -35,54 +43,76 @@ def get_device_name(device: aioshelly.Device) -> str: return device.settings["name"] or device.settings["device"]["hostname"] +def get_number_of_channels(device: aioshelly.Device, block: aioshelly.Block) -> int: + """Get number of channels for block type.""" + channels = None + + if block.type == "input": + # Shelly Dimmer/1L has two input channels and missing "num_inputs" + if device.settings["device"]["type"] in ["SHDM-1", "SHDM-2", "SHSW-L"]: + channels = 2 + else: + channels = device.shelly.get("num_inputs") + elif block.type == "emeter": + channels = device.shelly.get("num_emeters") + elif block.type in ["relay", "light"]: + channels = device.shelly.get("num_outputs") + elif block.type in ["roller", "device"]: + channels = 1 + + return channels or 1 + + def get_entity_name( device: aioshelly.Device, block: aioshelly.Block, description: Optional[str] = None, ) -> str: """Naming for switch and sensors.""" - entity_name = get_device_name(device) - - if block: - channels = None - if block.type == "input": - # Shelly Dimmer/1L has two input channels and missing "num_inputs" - if device.settings["device"]["type"] in ["SHDM-1", "SHDM-2", "SHSW-L"]: - channels = 2 - else: - channels = device.shelly.get("num_inputs") - elif block.type == "emeter": - channels = device.shelly.get("num_emeters") - elif block.type in ["relay", "light"]: - channels = device.shelly.get("num_outputs") - elif block.type in ["roller", "device"]: - channels = 1 - - channels = channels or 1 - - if channels > 1 and block.type != "device": - entity_name = None - mode = block.type + "s" - if mode in device.settings: - entity_name = device.settings[mode][int(block.channel)].get("name") - - if not entity_name: - if device.settings["device"]["type"] == "SHEM-3": - base = ord("A") - else: - base = ord("1") - entity_name = ( - f"{get_device_name(device)} channel {chr(int(block.channel)+base)}" - ) + channel_name = get_device_channel_name(device, block) if description: - entity_name = f"{entity_name} {description}" + return f"{channel_name} {description}" - return entity_name + return channel_name + + +def get_device_channel_name( + device: aioshelly.Device, + block: aioshelly.Block, +) -> str: + """Get name based on device and channel name.""" + entity_name = get_device_name(device) + + if ( + not block + or block.type == "device" + or get_number_of_channels(device, block) == 1 + ): + return entity_name + + channel_name = None + mode = block.type + "s" + if mode in device.settings: + channel_name = device.settings[mode][int(block.channel)].get("name") + + if channel_name: + return channel_name + + if device.settings["device"]["type"] == "SHEM-3": + base = ord("A") + else: + base = ord("1") + + return f"{entity_name} channel {chr(int(block.channel)+base)}" def is_momentary_input(settings: dict, block: aioshelly.Block) -> bool: """Return true if input button settings is set to a momentary type.""" + # Shelly Button type is fixed to momentary and no btn_type + if settings["device"]["type"] == "SHBTN-1": + return True + button = settings.get("relays") or settings.get("lights") or settings.get("inputs") # Shelly 1L has two button settings in the first channel @@ -108,3 +138,47 @@ def get_device_uptime(status: dict, last_uptime: str) -> str: return uptime.replace(microsecond=0).isoformat() return last_uptime + + +def get_input_triggers( + device: aioshelly.Device, block: aioshelly.Block +) -> List[Tuple[str, str]]: + """Return list of input triggers for block.""" + if "inputEvent" not in block.sensor_ids or "inputEventCnt" not in block.sensor_ids: + return [] + + if not is_momentary_input(device.settings, block): + return [] + + triggers = [] + + if block.type == "device" or get_number_of_channels(device, block) == 1: + subtype = "button" + else: + subtype = f"button{int(block.channel)+1}" + + if device.settings["device"]["type"] == "SHBTN-1": + trigger_types = SHBTN_1_INPUTS_EVENTS_TYPES + elif device.settings["device"]["type"] == "SHIX3-1": + trigger_types = SHIX3_1_INPUTS_EVENTS_TYPES + else: + trigger_types = BASIC_INPUTS_EVENTS_TYPES + + for trigger_type in trigger_types: + triggers.append((trigger_type, subtype)) + + return triggers + + +def get_device_wrapper(hass: HomeAssistant, device_id: str): + """Get a Shelly device wrapper for the given device id.""" + if not hass.data.get(DOMAIN): + return None + + for config_entry in hass.data[DOMAIN][DATA_CONFIG_ENTRY]: + wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry][COAP] + + if wrapper.device_id == device_id: + return wrapper + + return None diff --git a/homeassistant/components/shodan/manifest.json b/homeassistant/components/shodan/manifest.json index 32d05e2f90b..17f4dc1bf79 100644 --- a/homeassistant/components/shodan/manifest.json +++ b/homeassistant/components/shodan/manifest.json @@ -2,6 +2,6 @@ "domain": "shodan", "name": "Shodan", "documentation": "https://www.home-assistant.io/integrations/shodan", - "requirements": ["shodan==1.24.0"], + "requirements": ["shodan==1.25.0"], "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/shopping_list/__init__.py b/homeassistant/components/shopping_list/__init__.py index 8618e9bafb7..1831f894cec 100644 --- a/homeassistant/components/shopping_list/__init__.py +++ b/homeassistant/components/shopping_list/__init__.py @@ -128,6 +128,8 @@ async def async_setup_entry(hass, config_entry): SCHEMA_WEBSOCKET_CLEAR_ITEMS, ) + websocket_api.async_register_command(hass, websocket_handle_reorder) + return True @@ -163,6 +165,31 @@ class ShoppingData: self.items = [itm for itm in self.items if not itm["complete"]] await self.hass.async_add_executor_job(self.save) + @callback + def async_reorder(self, item_ids): + """Reorder items.""" + # The array for sorted items. + new_items = [] + all_items_mapping = {item["id"]: item for item in self.items} + # Append items by the order of passed in array. + for item_id in item_ids: + if item_id not in all_items_mapping: + raise KeyError + new_items.append(all_items_mapping[item_id]) + # Remove the item from mapping after it's appended in the result array. + del all_items_mapping[item_id] + # Append the rest of the items + for key in all_items_mapping: + # All the unchecked items must be passed in the item_ids array, + # so all items left in the mapping should be checked items. + if all_items_mapping[key]["complete"] is False: + raise vol.Invalid( + "The item ids array doesn't contain all the unchecked shopping list items." + ) + new_items.append(all_items_mapping[key]) + self.items = new_items + self.hass.async_add_executor_job(self.save) + async def async_load(self): """Load items.""" @@ -277,3 +304,26 @@ async def websocket_handle_clear(hass, connection, msg): await hass.data[DOMAIN].async_clear_completed() hass.bus.async_fire(EVENT, {"action": "clear"}) connection.send_message(websocket_api.result_message(msg["id"])) + + +@websocket_api.websocket_command( + { + vol.Required("type"): "shopping_list/items/reorder", + vol.Required("item_ids"): [str], + } +) +def websocket_handle_reorder(hass, connection, msg): + """Handle reordering shopping_list items.""" + msg_id = msg.pop("id") + try: + hass.data[DOMAIN].async_reorder(msg.pop("item_ids")) + hass.bus.async_fire(EVENT, {"action": "reorder"}) + connection.send_result(msg_id) + except KeyError: + connection.send_error( + msg_id, + websocket_api.const.ERR_NOT_FOUND, + "One or more item id(s) not found.", + ) + except vol.Invalid as err: + connection.send_error(msg_id, websocket_api.const.ERR_INVALID_FORMAT, f"{err}") diff --git a/homeassistant/components/shopping_list/translations/de.json b/homeassistant/components/shopping_list/translations/de.json index d2d6a42fe24..68372e9f4ac 100644 --- a/homeassistant/components/shopping_list/translations/de.json +++ b/homeassistant/components/shopping_list/translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Die Einkaufsliste ist bereits konfiguriert." + "already_configured": "Der Dienst ist bereits konfiguriert" }, "step": { "user": { diff --git a/homeassistant/components/shopping_list/translations/tr.json b/homeassistant/components/shopping_list/translations/tr.json new file mode 100644 index 00000000000..d139d2f6399 --- /dev/null +++ b/homeassistant/components/shopping_list/translations/tr.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "user": { + "description": "Al\u0131\u015fveri\u015f listesini yap\u0131land\u0131rmak istiyor musunuz?", + "title": "Al\u0131\u015fveri\u015f listesi" + } + } + }, + "title": "Al\u0131\u015fveri\u015f listesi" +} \ No newline at end of file diff --git a/homeassistant/components/shopping_list/translations/uk.json b/homeassistant/components/shopping_list/translations/uk.json new file mode 100644 index 00000000000..b73bd6c702a --- /dev/null +++ b/homeassistant/components/shopping_list/translations/uk.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "step": { + "user": { + "description": "\u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u0441\u043f\u0438\u0441\u043e\u043a \u043f\u043e\u043a\u0443\u043f\u043e\u043a?", + "title": "\u0421\u043f\u0438\u0441\u043e\u043a \u043f\u043e\u043a\u0443\u043f\u043e\u043a" + } + } + }, + "title": "\u0421\u043f\u0438\u0441\u043e\u043a \u043f\u043e\u043a\u0443\u043f\u043e\u043a" +} \ No newline at end of file diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json index a5c56b37778..99902b8dd36 100644 --- a/homeassistant/components/sighthound/manifest.json +++ b/homeassistant/components/sighthound/manifest.json @@ -2,6 +2,6 @@ "domain": "sighthound", "name": "Sighthound", "documentation": "https://www.home-assistant.io/integrations/sighthound", - "requirements": ["pillow==7.2.0", "simplehound==0.3"], + "requirements": ["pillow==8.1.0", "simplehound==0.3"], "codeowners": ["@robmarkcole"] } diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 89f5c40b1ff..495ba29fefb 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -10,10 +10,10 @@ from simplipy.websocket import ( EVENT_CONNECTION_LOST, EVENT_CONNECTION_RESTORED, EVENT_DOORBELL_DETECTED, - EVENT_ENTRY_DETECTED, + EVENT_ENTRY_DELAY, EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED, - EVENT_MOTION_DETECTED, + EVENT_SECRET_ALERT_TRIGGERED, ) import voluptuous as vol @@ -82,8 +82,8 @@ WEBSOCKET_EVENTS_REQUIRING_SERIAL = [EVENT_LOCK_LOCKED, EVENT_LOCK_UNLOCKED] WEBSOCKET_EVENTS_TO_TRIGGER_HASS_EVENT = [ EVENT_CAMERA_MOTION_DETECTED, EVENT_DOORBELL_DETECTED, - EVENT_ENTRY_DETECTED, - EVENT_MOTION_DETECTED, + EVENT_ENTRY_DELAY, + EVENT_SECRET_ALERT_TRIGGERED, ] ATTR_CATEGORY = "category" diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index a502a7908f0..b18bafb0bbf 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,6 +3,6 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==9.6.2"], + "requirements": ["simplisafe-python==9.6.4"], "codeowners": ["@bachya"] } diff --git a/homeassistant/components/simplisafe/translations/de.json b/homeassistant/components/simplisafe/translations/de.json index ab05cf649d8..5914e8f680c 100644 --- a/homeassistant/components/simplisafe/translations/de.json +++ b/homeassistant/components/simplisafe/translations/de.json @@ -1,17 +1,21 @@ { "config": { "abort": { - "already_configured": "Dieses SimpliSafe-Konto wird bereits verwendet." + "already_configured": "Dieses SimpliSafe-Konto wird bereits verwendet.", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich" }, "error": { "identifier_exists": "Konto bereits registriert", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { "reauth_confirm": { "data": { "password": "Passwort" - } + }, + "description": "Dein Zugriffstoken ist abgelaufen oder wurde widerrufen. Gib dein Passwort ein, um dein Konto erneut zu verkn\u00fcpfen.", + "title": "Integration erneut authentifizieren" }, "user": { "data": { diff --git a/homeassistant/components/simplisafe/translations/tr.json b/homeassistant/components/simplisafe/translations/tr.json index ec84b1b7c1c..94506fb426b 100644 --- a/homeassistant/components/simplisafe/translations/tr.json +++ b/homeassistant/components/simplisafe/translations/tr.json @@ -1,6 +1,26 @@ { "config": { + "abort": { + "already_configured": "Bu SimpliSafe hesab\u0131 zaten kullan\u0131mda.", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "still_awaiting_mfa": "Hala MFA e-posta t\u0131klamas\u0131 bekleniyor", + "unknown": "Beklenmeyen hata" + }, "step": { + "mfa": { + "description": "SimpliSafe'den bir ba\u011flant\u0131 i\u00e7in e-postan\u0131z\u0131 kontrol edin. Ba\u011flant\u0131y\u0131 do\u011frulad\u0131ktan sonra, entegrasyonun kurulumunu tamamlamak i\u00e7in buraya geri d\u00f6n\u00fcn.", + "title": "SimpliSafe \u00c7ok Fakt\u00f6rl\u00fc Kimlik Do\u011frulama" + }, + "reauth_confirm": { + "data": { + "password": "Parola" + }, + "description": "Eri\u015fim kodunuzun s\u00fcresi doldu veya iptal edildi. Hesab\u0131n\u0131z\u0131 yeniden ba\u011flamak i\u00e7in parolan\u0131z\u0131 girin.", + "title": "Entegrasyonu Yeniden Do\u011frula" + }, "user": { "data": { "password": "Parola", diff --git a/homeassistant/components/simplisafe/translations/uk.json b/homeassistant/components/simplisafe/translations/uk.json index 376fb4468db..0a51f129e5f 100644 --- a/homeassistant/components/simplisafe/translations/uk.json +++ b/homeassistant/components/simplisafe/translations/uk.json @@ -1,18 +1,45 @@ { "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e" + }, + "error": { + "identifier_exists": "\u041e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u043e\u0432\u0430\u043d\u043e.", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "still_awaiting_mfa": "\u041e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f, \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u043e\u0433\u043e \u043f\u043e \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0456\u0439 \u043f\u043e\u0448\u0442\u0456.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, "step": { + "mfa": { + "description": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0441\u0432\u043e\u044e \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u0443 \u043f\u043e\u0448\u0442\u0443 \u043d\u0430 \u043d\u0430\u044f\u0432\u043d\u0456\u0441\u0442\u044c \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0432\u0456\u0434 SimpliSafe. \u041f\u0456\u0441\u043b\u044f \u0442\u043e\u0433\u043e \u044f\u043a \u0432\u0456\u0434\u043a\u0440\u0438\u0454\u0442\u0435 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f, \u043f\u043e\u0432\u0435\u0440\u043d\u0456\u0442\u044c\u0441\u044f \u0441\u044e\u0434\u0438, \u0449\u043e\u0431 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0443 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457.", + "title": "\u0414\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f SimpliSafe" + }, "reauth_confirm": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c" - } + }, + "description": "\u0412\u0430\u0448 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443 \u0437\u0430\u043a\u0456\u043d\u0447\u0438\u0432\u0441\u044f \u0430\u0431\u043e \u0431\u0443\u0432 \u0430\u043d\u0443\u043b\u044c\u043e\u0432\u0430\u043d\u0438\u0439. \u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u0430\u0440\u043e\u043b\u044c, \u0449\u043e\u0431 \u0437\u0430\u043d\u043e\u0432\u043e \u043f\u0440\u0438\u0432'\u044f\u0437\u0430\u0442\u0438 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0432\u0430\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e" }, "user": { "data": { + "code": "\u041a\u043e\u0434 (\u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0432 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0456 Home Assistant)", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438" }, "title": "\u0417\u0430\u043f\u043e\u0432\u043d\u0456\u0442\u044c \u0432\u0430\u0448\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e" } } + }, + "options": { + "step": { + "init": { + "data": { + "code": "\u041a\u043e\u0434 (\u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0432 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0456 Home Assistant)" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f SimpliSafe" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/slack/__init__.py b/homeassistant/components/slack/__init__.py index a999c2375ff..e3ae111f2ea 100644 --- a/homeassistant/components/slack/__init__.py +++ b/homeassistant/components/slack/__init__.py @@ -1 +1,2 @@ """The slack component.""" +DOMAIN = "slack" diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py index 90caad62a58..985f59a6715 100644 --- a/homeassistant/components/slack/notify.py +++ b/homeassistant/components/slack/notify.py @@ -1,7 +1,10 @@ """Slack platform for notify component.""" +from __future__ import annotations + import asyncio import logging import os +from typing import Any, List, Optional, TypedDict from urllib.parse import urlparse from aiohttp import BasicAuth, FormData @@ -21,6 +24,11 @@ from homeassistant.const import CONF_API_KEY, CONF_ICON, CONF_USERNAME from homeassistant.core import callback from homeassistant.helpers import aiohttp_client, config_validation as cv import homeassistant.helpers.template as template +from homeassistant.helpers.typing import ( + ConfigType, + DiscoveryInfoType, + HomeAssistantType, +) _LOGGER = logging.getLogger(__name__) @@ -74,7 +82,38 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -async def async_get_service(hass, config, discovery_info=None): +class AuthDictT(TypedDict, total=False): + """Type for auth request data.""" + + auth: BasicAuth + + +class FormDataT(TypedDict): + """Type for form data, file upload.""" + + channels: str + filename: str + initial_comment: str + title: str + token: str + + +class MessageT(TypedDict, total=False): + """Type for message data.""" + + link_names: bool + text: str + username: str # Optional key + icon_url: str # Optional key + icon_emoji: str # Optional key + blocks: List[Any] # Optional key + + +async def async_get_service( + hass: HomeAssistantType, + config: ConfigType, + discovery_info: Optional[DiscoveryInfoType] = None, +) -> Optional[SlackNotificationService]: """Set up the Slack notification service.""" session = aiohttp_client.async_get_clientsession(hass) client = WebClient(token=config[CONF_API_KEY], run_async=True, session=session) @@ -82,8 +121,14 @@ async def async_get_service(hass, config, discovery_info=None): try: await client.auth_test() except SlackApiError as err: - _LOGGER.error("Error while setting up integration: %s", err) - return + _LOGGER.error("Error while setting up integration: %r", err) + return None + except ClientError as err: + _LOGGER.warning( + "Error testing connection to slack: %r " + "Continuing setup anyway, but notify service might not work", + err, + ) return SlackNotificationService( hass, @@ -95,20 +140,20 @@ async def async_get_service(hass, config, discovery_info=None): @callback -def _async_get_filename_from_url(url): +def _async_get_filename_from_url(url: str) -> str: """Return the filename of a passed URL.""" parsed_url = urlparse(url) return os.path.basename(parsed_url.path) @callback -def _async_sanitize_channel_names(channel_list): +def _async_sanitize_channel_names(channel_list: List[str]) -> List[str]: """Remove any # symbols from a channel list.""" return [channel.lstrip("#") for channel in channel_list] @callback -def _async_templatize_blocks(hass, value): +def _async_templatize_blocks(hass: HomeAssistantType, value: Any) -> Any: """Recursive template creator helper function.""" if isinstance(value, list): return [_async_templatize_blocks(hass, item) for item in value] @@ -117,14 +162,21 @@ def _async_templatize_blocks(hass, value): key: _async_templatize_blocks(hass, item) for key, item in value.items() } - tmpl = template.Template(value, hass=hass) + tmpl = template.Template(value, hass=hass) # type: ignore # no-untyped-call return tmpl.async_render(parse_result=False) class SlackNotificationService(BaseNotificationService): """Define the Slack notification logic.""" - def __init__(self, hass, client, default_channel, username, icon): + def __init__( + self, + hass: HomeAssistantType, + client: WebClient, + default_channel: str, + username: Optional[str], + icon: Optional[str], + ) -> None: """Initialize.""" self._client = client self._default_channel = default_channel @@ -132,7 +184,13 @@ class SlackNotificationService(BaseNotificationService): self._icon = icon self._username = username - async def _async_send_local_file_message(self, path, targets, message, title): + async def _async_send_local_file_message( + self, + path: str, + targets: List[str], + message: str, + title: Optional[str], + ) -> None: """Upload a local file (with message) to Slack.""" if not self._hass.config.is_allowed_path(path): _LOGGER.error("Path does not exist or is not allowed: %s", path) @@ -149,12 +207,19 @@ class SlackNotificationService(BaseNotificationService): initial_comment=message, title=title or filename, ) - except SlackApiError as err: - _LOGGER.error("Error while uploading file-based message: %s", err) + except (SlackApiError, ClientError) as err: + _LOGGER.error("Error while uploading file-based message: %r", err) async def _async_send_remote_file_message( - self, url, targets, message, title, *, username=None, password=None - ): + self, + url: str, + targets: List[str], + message: str, + title: Optional[str], + *, + username: Optional[str] = None, + password: Optional[str] = None, + ) -> None: """Upload a remote file (with message) to Slack. Note that we bypass the python-slackclient WebClient and use aiohttp directly, @@ -166,9 +231,9 @@ class SlackNotificationService(BaseNotificationService): return filename = _async_get_filename_from_url(url) - session = aiohttp_client.async_get_clientsession(self.hass) + session = aiohttp_client.async_get_clientsession(self._hass) - kwargs = {} + kwargs: AuthDictT = {} if username and password is not None: kwargs = {"auth": BasicAuth(username, password=password)} @@ -177,49 +242,46 @@ class SlackNotificationService(BaseNotificationService): try: resp.raise_for_status() except ClientError as err: - _LOGGER.error("Error while retrieving %s: %s", url, err) + _LOGGER.error("Error while retrieving %s: %r", url, err) return - data = FormData( - { - "channels": ",".join(targets), - "filename": filename, - "initial_comment": message, - "title": title or filename, - "token": self._client.token, - }, - charset="utf-8", - ) + form_data: FormDataT = { + "channels": ",".join(targets), + "filename": filename, + "initial_comment": message, + "title": title or filename, + "token": self._client.token, + } + + data = FormData(form_data, charset="utf-8") data.add_field("file", resp.content, filename=filename) try: await session.post("https://slack.com/api/files.upload", data=data) except ClientError as err: - _LOGGER.error("Error while uploading file message: %s", err) + _LOGGER.error("Error while uploading file message: %r", err) async def _async_send_text_only_message( self, - targets, - message, - title, + targets: List[str], + message: str, + title: Optional[str], *, - username=None, - icon=None, - blocks=None, - ): + username: Optional[str] = None, + icon: Optional[str] = None, + blocks: Optional[Any] = None, + ) -> None: """Send a text-only message.""" - message_dict = {"link_names": True, "text": message} + message_dict: MessageT = {"link_names": True, "text": message} if username: message_dict["username"] = username if icon: if icon.lower().startswith(("http://", "https://")): - icon_type = "url" + message_dict["icon_url"] = icon else: - icon_type = "emoji" - - message_dict[f"icon_{icon_type}"] = icon + message_dict["icon_emoji"] = icon if blocks: message_dict["blocks"] = blocks @@ -233,17 +295,16 @@ class SlackNotificationService(BaseNotificationService): for target, result in zip(tasks, results): if isinstance(result, SlackApiError): _LOGGER.error( - "There was a Slack API error while sending to %s: %s", + "There was a Slack API error while sending to %s: %r", target, result, ) + elif isinstance(result, ClientError): + _LOGGER.error("Error while sending message to %s: %r", target, result) - async def async_send_message(self, message, **kwargs): + async def async_send_message(self, message: str, **kwargs: Any) -> None: """Send a message to Slack.""" - data = kwargs.get(ATTR_DATA) - - if data is None: - data = {} + data = kwargs.get(ATTR_DATA) or {} try: DATA_SCHEMA(data) @@ -259,7 +320,9 @@ class SlackNotificationService(BaseNotificationService): # Message Type 1: A text-only message if ATTR_FILE not in data: if ATTR_BLOCKS_TEMPLATE in data: - blocks = _async_templatize_blocks(self.hass, data[ATTR_BLOCKS_TEMPLATE]) + blocks = _async_templatize_blocks( + self._hass, data[ATTR_BLOCKS_TEMPLATE] + ) elif ATTR_BLOCKS in data: blocks = data[ATTR_BLOCKS] else: diff --git a/homeassistant/components/smappee/manifest.json b/homeassistant/components/smappee/manifest.json index ba1005b87d4..ddbff4e7738 100644 --- a/homeassistant/components/smappee/manifest.json +++ b/homeassistant/components/smappee/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/smappee", "dependencies": ["http"], "requirements": [ - "pysmappee==0.2.13" + "pysmappee==0.2.16" ], "codeowners": [ "@bsmappee" diff --git a/homeassistant/components/smappee/translations/de.json b/homeassistant/components/smappee/translations/de.json index a609492f428..15fd8d6cd22 100644 --- a/homeassistant/components/smappee/translations/de.json +++ b/homeassistant/components/smappee/translations/de.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "cannot_connect": "Verbindungsfehler" + "already_configured_device": "Ger\u00e4t ist bereits konfiguriert", + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "cannot_connect": "Verbindung fehlgeschlagen", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url})." }, "flow_title": "Smappee: {name}", "step": { @@ -9,6 +13,14 @@ "data": { "environment": "Umgebung" } + }, + "local": { + "data": { + "host": "Host" + } + }, + "pick_implementation": { + "title": "W\u00e4hle die Authentifizierungsmethode" } } } diff --git a/homeassistant/components/smappee/translations/tr.json b/homeassistant/components/smappee/translations/tr.json new file mode 100644 index 00000000000..4ba8a4da9a6 --- /dev/null +++ b/homeassistant/components/smappee/translations/tr.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured_device": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_configured_local_device": "Yerel ayg\u0131t (lar) zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. L\u00fctfen bir bulut cihaz\u0131n\u0131 yap\u0131land\u0131rmadan \u00f6nce bunlar\u0131 kald\u0131r\u0131n.", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_mdns": "Smappee entegrasyonu i\u00e7in desteklenmeyen cihaz." + }, + "flow_title": "Smappee: {name}", + "step": { + "environment": { + "data": { + "environment": "\u00c7evre" + } + }, + "local": { + "data": { + "host": "Ana Bilgisayar" + } + }, + "zeroconf_confirm": { + "title": "Smappee cihaz\u0131 bulundu" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smappee/translations/uk.json b/homeassistant/components/smappee/translations/uk.json new file mode 100644 index 00000000000..a268fa82eac --- /dev/null +++ b/homeassistant/components/smappee/translations/uk.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured_device": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_configured_local_device": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435 \u0434\u043b\u044f \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432. \u041d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u0432\u0438\u0434\u0430\u043b\u0438\u0442\u0438 \u0457\u0445 \u043f\u0435\u0440\u0435\u0434 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0445\u043c\u0430\u0440\u043d\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e.", + "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_mdns": "\u041d\u0435\u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0438\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439.", + "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443." + }, + "flow_title": "Smappee: {name}", + "step": { + "environment": { + "data": { + "environment": "\u041e\u0442\u043e\u0447\u0435\u043d\u043d\u044f" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 Smappee." + }, + "local": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0430\u0434\u0440\u0435\u0441\u0443 \u0445\u043e\u0441\u0442\u0430, \u0449\u043e\u0431 \u043f\u043e\u0447\u0430\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e \u0437 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u0438\u043c \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c Smappee" + }, + "pick_implementation": { + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + }, + "zeroconf_confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 Smappee \u0437 \u0441\u0435\u0440\u0456\u0439\u043d\u0438\u043c \u043d\u043e\u043c\u0435\u0440\u043e\u043c `{serialnumber}`?", + "title": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 Smappee" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smart_meter_texas/translations/de.json b/homeassistant/components/smart_meter_texas/translations/de.json index 38215675701..0eee2778d05 100644 --- a/homeassistant/components/smart_meter_texas/translations/de.json +++ b/homeassistant/components/smart_meter_texas/translations/de.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/smart_meter_texas/translations/tr.json b/homeassistant/components/smart_meter_texas/translations/tr.json new file mode 100644 index 00000000000..6ed28a58c79 --- /dev/null +++ b/homeassistant/components/smart_meter_texas/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smart_meter_texas/translations/uk.json b/homeassistant/components/smart_meter_texas/translations/uk.json new file mode 100644 index 00000000000..49bceaa3f6e --- /dev/null +++ b/homeassistant/components/smart_meter_texas/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/de.json b/homeassistant/components/smarthab/translations/de.json index 2c76c4d56db..18bb2c77047 100644 --- a/homeassistant/components/smarthab/translations/de.json +++ b/homeassistant/components/smarthab/translations/de.json @@ -1,6 +1,7 @@ { "config": { "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/smarthab/translations/tr.json b/homeassistant/components/smarthab/translations/tr.json new file mode 100644 index 00000000000..98da6384f8d --- /dev/null +++ b/homeassistant/components/smarthab/translations/tr.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "email": "E-posta", + "password": "Parola" + }, + "title": "SmartHab'\u0131 kurun" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smarthab/translations/uk.json b/homeassistant/components/smarthab/translations/uk.json new file mode 100644 index 00000000000..036ec0a78d4 --- /dev/null +++ b/homeassistant/components/smarthab/translations/uk.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "service": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0441\u043f\u0440\u043e\u0431\u0456 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e SmartHab. \u0421\u0435\u0440\u0432\u0456\u0441 \u043c\u043e\u0436\u0435 \u0431\u0443\u0442\u0438 \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "email": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c" + }, + "description": "\u0417 \u0442\u0435\u0445\u043d\u0456\u0447\u043d\u0438\u0445 \u043f\u0440\u0438\u0447\u0438\u043d \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u043e \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0438\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0434\u043b\u044f Home Assistant. \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0441\u0442\u0432\u043e\u0440\u0438\u0442\u0438 \u0457\u0457 \u0432 \u0434\u043e\u0434\u0430\u0442\u043a\u0443 SmartHab.", + "title": "SmartHab" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/translations/tr.json b/homeassistant/components/smartthings/translations/tr.json new file mode 100644 index 00000000000..5e7463c1c74 --- /dev/null +++ b/homeassistant/components/smartthings/translations/tr.json @@ -0,0 +1,17 @@ +{ + "config": { + "error": { + "webhook_error": "SmartThings, webhook URL'sini do\u011frulayamad\u0131. L\u00fctfen webhook URL'sinin internetten eri\u015filebilir oldu\u011fundan emin olun ve tekrar deneyin." + }, + "step": { + "pat": { + "data": { + "access_token": "Eri\u015fim Belirteci" + } + }, + "select_location": { + "title": "Konum Se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smartthings/translations/uk.json b/homeassistant/components/smartthings/translations/uk.json new file mode 100644 index 00000000000..6f8a0ed4744 --- /dev/null +++ b/homeassistant/components/smartthings/translations/uk.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "invalid_webhook_url": "Webhook URL, \u0432\u043a\u0430\u0437\u0430\u043d\u0438\u0439 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u044c \u0432\u0456\u0434 SmartThings, \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439:\n > {webhook_url} \n\n\u041e\u043d\u043e\u0432\u0456\u0442\u044c \u0432\u0430\u0448\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u043d\u043e \u0434\u043e [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0439] ({component_url}), \u0430 \u043f\u0456\u0441\u043b\u044f \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a\u0443 Home Assistant \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437.", + "no_available_locations": "\u041d\u0435\u043c\u0430\u0454 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0445 \u0434\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043c\u0456\u0441\u0446\u044c \u0440\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f SmartThings." + }, + "error": { + "app_setup_error": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 SmartApp. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437.", + "token_forbidden": "\u0422\u043e\u043a\u0435\u043d \u043d\u0435 \u043c\u043e\u0436\u0435 \u0431\u0443\u0442\u0438 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u0438\u0439 \u0434\u043b\u044f OAuth.", + "token_invalid_format": "\u0422\u043e\u043a\u0435\u043d \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 UID / GUID.", + "token_unauthorized": "\u0422\u043e\u043a\u0435\u043d \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439 \u0430\u0431\u043e \u0431\u0456\u043b\u044c\u0448\u0435 \u043d\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d\u0438\u0439.", + "webhook_error": "SmartThings \u043d\u0435 \u043c\u043e\u0436\u0435 \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0438\u0442\u0438 Webhook URL. \u041f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0432\u043a\u0430\u0437\u0430\u043d\u0438\u0439 Webhook URL \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0456\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0456 \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437." + }, + "step": { + "authorize": { + "title": "\u0410\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u044f Home Assistant" + }, + "pat": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c [\u041e\u0441\u043e\u0431\u0438\u0441\u0442\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443 SmartThings] ({token_url}), \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u0438\u0439 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u043d\u043e \u0434\u043e [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0457] ({component_url}).", + "title": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043f\u0435\u0440\u0441\u043e\u043d\u0430\u043b\u044c\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443" + }, + "select_location": { + "data": { + "location_id": "\u041c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043c\u0456\u0441\u0446\u0435 \u0440\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f SmartThings, \u044f\u043a\u0438\u0439 \u0432\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 \u0432 Home Assistant. \u041f\u0456\u0441\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u0432\u0456\u0434\u043a\u0440\u0438\u0454\u0442\u044c\u0441\u044f \u043d\u043e\u0432\u0435 \u0432\u0456\u043a\u043d\u043e, \u0434\u0435 \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0431\u0443\u0434\u0435 \u0443\u0432\u0456\u0439\u0442\u0438 \u0432 \u0441\u0438\u0441\u0442\u0435\u043c\u0443 \u0442\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0432\u0430\u0442\u0438 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0443 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 Home Assistant \u0432 \u043e\u0431\u0440\u0430\u043d\u043e\u043c\u0443 \u043c\u0456\u0441\u0446\u0456 \u0440\u043e\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043d\u043d\u044f.", + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f" + }, + "user": { + "description": "SmartThings \u0431\u0443\u0434\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439 \u0434\u043b\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043a\u0438 push-\u043e\u043d\u043e\u0432\u043b\u0435\u043d\u044c \u0437\u0430 \u0430\u0434\u0440\u0435\u0441\u043e\u044e:\n> {webhook_url} \n\n\u042f\u043a\u0449\u043e \u0446\u0435 \u043d\u0435 \u0442\u0430\u043a, \u043e\u043d\u043e\u0432\u0456\u0442\u044c \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e, \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u0442\u0456\u0442\u044c Home Assistant \u0456 \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437.", + "title": "\u041f\u0456\u0434\u0442\u0432\u0435\u0440\u0434\u0436\u0435\u043d\u043d\u044f Callback URL" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/smhi/translations/uk.json b/homeassistant/components/smhi/translations/uk.json new file mode 100644 index 00000000000..24af32172ba --- /dev/null +++ b/homeassistant/components/smhi/translations/uk.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "name_exists": "\u0426\u044f \u043d\u0430\u0437\u0432\u0430 \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f.", + "wrong_location": "\u0422\u0456\u043b\u044c\u043a\u0438 \u0434\u043b\u044f \u0428\u0432\u0435\u0446\u0456\u0457." + }, + "step": { + "user": { + "data": { + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u0432\u0433\u043e\u0442\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "title": "\u041c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432 \u0428\u0432\u0435\u0446\u0456\u0457" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sms/translations/de.json b/homeassistant/components/sms/translations/de.json index 1252313a438..b262df1486d 100644 --- a/homeassistant/components/sms/translations/de.json +++ b/homeassistant/components/sms/translations/de.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", "unknown": "Unerwarteter Fehler" }, "step": { diff --git a/homeassistant/components/sms/translations/tr.json b/homeassistant/components/sms/translations/tr.json new file mode 100644 index 00000000000..1ef2efb8121 --- /dev/null +++ b/homeassistant/components/sms/translations/tr.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "title": "Modeme ba\u011flan\u0131n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sms/translations/uk.json b/homeassistant/components/sms/translations/uk.json new file mode 100644 index 00000000000..be271a2b6e4 --- /dev/null +++ b/homeassistant/components/sms/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "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." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "device": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + }, + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/snmp/sensor.py b/homeassistant/components/snmp/sensor.py index 9ca0444f7bc..a60183a1a0f 100644 --- a/homeassistant/components/snmp/sensor.py +++ b/homeassistant/components/snmp/sensor.py @@ -210,4 +210,4 @@ class SnmpData: self.value = self._default_value else: for resrow in restable: - self.value = str(resrow[-1]) + self.value = resrow[-1].prettyPrint() diff --git a/homeassistant/components/solaredge/manifest.json b/homeassistant/components/solaredge/manifest.json index 59b8cba7446..f0a620021ad 100644 --- a/homeassistant/components/solaredge/manifest.json +++ b/homeassistant/components/solaredge/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/solaredge", "requirements": ["solaredge==0.0.2", "stringcase==1.2.0"], "config_flow": true, - "codeowners": [] + "codeowners": [], + "dhcp": [{"hostname":"target","macaddress":"002702*"}] } diff --git a/homeassistant/components/solaredge/translations/fr.json b/homeassistant/components/solaredge/translations/fr.json index f8aec1fa230..6fa6fdf264f 100644 --- a/homeassistant/components/solaredge/translations/fr.json +++ b/homeassistant/components/solaredge/translations/fr.json @@ -4,7 +4,9 @@ "site_exists": "Ce site est d\u00e9j\u00e0 configur\u00e9" }, "error": { - "site_exists": "Ce site est d\u00e9j\u00e0 configur\u00e9" + "invalid_api_key": "Cl\u00e9 API invalide", + "site_exists": "Ce site est d\u00e9j\u00e0 configur\u00e9", + "site_not_active": "The site n'est pas actif" }, "step": { "user": { diff --git a/homeassistant/components/solaredge/translations/lb.json b/homeassistant/components/solaredge/translations/lb.json index 4f2f698a6ca..709a57f070b 100644 --- a/homeassistant/components/solaredge/translations/lb.json +++ b/homeassistant/components/solaredge/translations/lb.json @@ -1,10 +1,13 @@ { "config": { "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert", "site_exists": "D\u00ebs site_id ass scho konfigur\u00e9iert" }, "error": { - "site_exists": "D\u00ebs site_id ass scho konfigur\u00e9iert" + "already_configured": "Apparat ass scho konfigur\u00e9iert", + "site_exists": "D\u00ebs site_id ass scho konfigur\u00e9iert", + "site_not_active": "De Site ass net aktiv" }, "step": { "user": { diff --git a/homeassistant/components/solaredge/translations/tr.json b/homeassistant/components/solaredge/translations/tr.json index 5307276a71d..b8159be58b4 100644 --- a/homeassistant/components/solaredge/translations/tr.json +++ b/homeassistant/components/solaredge/translations/tr.json @@ -2,6 +2,19 @@ "config": { "abort": { "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "could_not_connect": "Solaredge API'ye ba\u011flan\u0131lamad\u0131", + "invalid_api_key": "Ge\u00e7ersiz API anahtar\u0131", + "site_not_active": "Site aktif de\u011fil" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/solaredge/translations/uk.json b/homeassistant/components/solaredge/translations/uk.json new file mode 100644 index 00000000000..5ad67d87680 --- /dev/null +++ b/homeassistant/components/solaredge/translations/uk.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "site_exists": "\u0426\u0435\u0439 site_id \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439." + }, + "error": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "could_not_connect": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0456\u0437 API Solaredge.", + "invalid_api_key": "\u0425\u0438\u0431\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 API", + "site_exists": "\u0426\u0435\u0439 site_id \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439.", + "site_not_active": "\u0421\u0430\u0439\u0442 \u043d\u0435 \u0430\u043a\u0442\u0438\u0432\u043d\u0438\u0439." + }, + "step": { + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "name": "\u041d\u0430\u0437\u0432\u0430", + "site_id": "site-id" + }, + "title": "SolarEdge" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/translations/de.json b/homeassistant/components/solarlog/translations/de.json index 58e691b733d..008e1058681 100644 --- a/homeassistant/components/solarlog/translations/de.json +++ b/homeassistant/components/solarlog/translations/de.json @@ -5,7 +5,7 @@ }, "error": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "cannot_connect": "Verbindung fehlgeschlagen. \u00dcberpr\u00fcfe die Host-Adresse" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "user": { diff --git a/homeassistant/components/solarlog/translations/tr.json b/homeassistant/components/solarlog/translations/tr.json new file mode 100644 index 00000000000..a11d3815eed --- /dev/null +++ b/homeassistant/components/solarlog/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/translations/uk.json b/homeassistant/components/solarlog/translations/uk.json new file mode 100644 index 00000000000..f4fca695032 --- /dev/null +++ b/homeassistant/components/solarlog/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041f\u0440\u0435\u0444\u0456\u043a\u0441, \u044f\u043a\u0438\u0439 \u0431\u0443\u0434\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u0434\u043b\u044f \u0441\u0435\u043d\u0441\u043e\u0440\u0456\u0432 Solar-Log" + }, + "title": "Solar-Log" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/__init__.py b/homeassistant/components/soma/__init__.py index 3439684f977..bd5695cb7ec 100644 --- a/homeassistant/components/soma/__init__.py +++ b/homeassistant/components/soma/__init__.py @@ -1,8 +1,7 @@ """Support for Soma Smartshades.""" -import logging +import asyncio from api.soma_api import SomaApi -from requests import RequestException import voluptuous as vol from homeassistant import config_entries @@ -16,8 +15,6 @@ from .const import API, DOMAIN, HOST, PORT DEVICES = "devices" -_LOGGER = logging.getLogger(__name__) - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -63,7 +60,16 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): """Unload a config entry.""" - return True + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in SOMA_COMPONENTS + ] + ) + ) + + return unload_ok class SomaEntity(Entity): @@ -103,43 +109,3 @@ class SomaEntity(Entity): "name": self.name, "manufacturer": "Wazombi Labs", } - - async def async_update(self): - """Update the device with the latest data.""" - try: - response = await self.hass.async_add_executor_job( - self.api.get_shade_state, self.device["mac"] - ) - except RequestException: - _LOGGER.error("Connection to SOMA Connect failed") - self.is_available = False - return - if response["result"] != "success": - _LOGGER.error( - "Unable to reach device %s (%s)", self.device["name"], response["msg"] - ) - self.is_available = False - return - self.current_position = 100 - response["position"] - try: - response = await self.hass.async_add_executor_job( - self.api.get_battery_level, self.device["mac"] - ) - except RequestException: - _LOGGER.error("Connection to SOMA Connect failed") - self.is_available = False - return - if response["result"] != "success": - _LOGGER.error( - "Unable to reach device %s (%s)", self.device["name"], response["msg"] - ) - self.is_available = False - return - # https://support.somasmarthome.com/hc/en-us/articles/360026064234-HTTP-API - # battery_level response is expected to be min = 360, max 410 for - # 0-100% levels above 410 are consider 100% and below 360, 0% as the - # device considers 360 the minimum to move the motor. - _battery = round(2 * (response["battery_level"] - 360)) - battery = max(min(100, _battery), 0) - self.battery_state = battery - self.is_available = True diff --git a/homeassistant/components/soma/cover.py b/homeassistant/components/soma/cover.py index f2929dd8ddd..1005bf32f20 100644 --- a/homeassistant/components/soma/cover.py +++ b/homeassistant/components/soma/cover.py @@ -2,6 +2,8 @@ import logging +from requests import RequestException + from homeassistant.components.cover import ATTR_POSITION, CoverEntity from homeassistant.components.soma import API, DEVICES, DOMAIN, SomaEntity @@ -67,3 +69,23 @@ class SomaCover(SomaEntity, CoverEntity): def is_closed(self): """Return if the cover is closed.""" return self.current_position == 0 + + async def async_update(self): + """Update the cover with the latest data.""" + try: + _LOGGER.debug("Soma Cover Update") + response = await self.hass.async_add_executor_job( + self.api.get_shade_state, self.device["mac"] + ) + except RequestException: + _LOGGER.error("Connection to SOMA Connect failed") + self.is_available = False + return + if response["result"] != "success": + _LOGGER.error( + "Unable to reach device %s (%s)", self.device["name"], response["msg"] + ) + self.is_available = False + return + self.current_position = 100 - response["position"] + self.is_available = True diff --git a/homeassistant/components/soma/sensor.py b/homeassistant/components/soma/sensor.py index 2d37a0b0dce..9430a929e1e 100644 --- a/homeassistant/components/soma/sensor.py +++ b/homeassistant/components/soma/sensor.py @@ -1,10 +1,20 @@ """Support for Soma sensors.""" +from datetime import timedelta +import logging + +from requests import RequestException + from homeassistant.const import DEVICE_CLASS_BATTERY, PERCENTAGE from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle from . import DEVICES, SomaEntity from .const import API, DOMAIN +MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=30) + +_LOGGER = logging.getLogger(__name__) + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Soma sensor platform.""" @@ -38,3 +48,30 @@ class SomaSensor(SomaEntity, Entity): def unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" return PERCENTAGE + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + async def async_update(self): + """Update the sensor with the latest data.""" + try: + _LOGGER.debug("Soma Sensor Update") + response = await self.hass.async_add_executor_job( + self.api.get_battery_level, self.device["mac"] + ) + except RequestException: + _LOGGER.error("Connection to SOMA Connect failed") + self.is_available = False + return + if response["result"] != "success": + _LOGGER.error( + "Unable to reach device %s (%s)", self.device["name"], response["msg"] + ) + self.is_available = False + return + # https://support.somasmarthome.com/hc/en-us/articles/360026064234-HTTP-API + # battery_level response is expected to be min = 360, max 410 for + # 0-100% levels above 410 are consider 100% and below 360, 0% as the + # device considers 360 the minimum to move the motor. + _battery = round(2 * (response["battery_level"] - 360)) + battery = max(min(100, _battery), 0) + self.battery_state = battery + self.is_available = True diff --git a/homeassistant/components/soma/translations/tr.json b/homeassistant/components/soma/translations/tr.json new file mode 100644 index 00000000000..21a477c75a7 --- /dev/null +++ b/homeassistant/components/soma/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/translations/uk.json b/homeassistant/components/soma/translations/uk.json new file mode 100644 index 00000000000..0ec98301d62 --- /dev/null +++ b/homeassistant/components/soma/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_setup": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435.", + "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "connection_error": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0456\u0437 SOMA Connect.", + "missing_configuration": "\u041a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 Soma \u043d\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", + "result_error": "SOMA Connect \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0432 \u0437\u0456 \u0441\u0442\u0430\u0442\u0443\u0441\u043e\u043c \u043f\u043e\u043c\u0438\u043b\u043a\u0438." + }, + "create_entry": { + "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e SOMA Connect.", + "title": "SOMA Connect" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/manifest.json b/homeassistant/components/somfy/manifest.json index ea84bf34586..a236bc40085 100644 --- a/homeassistant/components/somfy/manifest.json +++ b/homeassistant/components/somfy/manifest.json @@ -5,5 +5,8 @@ "documentation": "https://www.home-assistant.io/integrations/somfy", "dependencies": ["http"], "codeowners": ["@tetienne"], - "requirements": ["pymfy==0.9.3"] -} \ No newline at end of file + "requirements": ["pymfy==0.9.3"], + "zeroconf": [ + {"type": "_kizbox._tcp.local.", "name": "gateway*"} + ] +} diff --git a/homeassistant/components/somfy/translations/de.json b/homeassistant/components/somfy/translations/de.json index 6b76e2f61be..29a959f48ce 100644 --- a/homeassistant/components/somfy/translations/de.json +++ b/homeassistant/components/somfy/translations/de.json @@ -2,10 +2,12 @@ "config": { "abort": { "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", - "missing_configuration": "Die Somfy-Komponente ist nicht konfiguriert. Folge bitte der Dokumentation." + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "create_entry": { - "default": "Erfolgreich mit Somfy authentifiziert." + "default": "Erfolgreich authentifiziert" }, "step": { "pick_implementation": { diff --git a/homeassistant/components/somfy/translations/tr.json b/homeassistant/components/somfy/translations/tr.json new file mode 100644 index 00000000000..a152eb19468 --- /dev/null +++ b/homeassistant/components/somfy/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/uk.json b/homeassistant/components/somfy/translations/uk.json new file mode 100644 index 00000000000..ebf7e41044e --- /dev/null +++ b/homeassistant/components/somfy/translations/uk.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.", + "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." + }, + "create_entry": { + "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." + }, + "step": { + "pick_implementation": { + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/__init__.py b/homeassistant/components/somfy_mylink/__init__.py index 8106cde0c18..d15ea029530 100644 --- a/homeassistant/components/somfy_mylink/__init__.py +++ b/homeassistant/components/somfy_mylink/__init__.py @@ -1,18 +1,35 @@ """Component for the Somfy MyLink device supporting the Synergy API.""" +import asyncio +import logging + from somfy_mylink_synergy import SomfyMyLinkSynergy import voluptuous as vol +from homeassistant.components.cover import ENTITY_ID_FORMAT +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.discovery import async_load_platform +from homeassistant.util import slugify -CONF_ENTITY_CONFIG = "entity_config" -CONF_SYSTEM_ID = "system_id" -CONF_REVERSE = "reverse" -CONF_DEFAULT_REVERSE = "default_reverse" -DATA_SOMFY_MYLINK = "somfy_mylink_data" -DOMAIN = "somfy_mylink" -SOMFY_MYLINK_COMPONENTS = ["cover"] +from .const import ( + CONF_DEFAULT_REVERSE, + CONF_ENTITY_CONFIG, + CONF_REVERSE, + CONF_REVERSED_TARGET_IDS, + CONF_SYSTEM_ID, + DATA_SOMFY_MYLINK, + DEFAULT_PORT, + DOMAIN, + MYLINK_STATUS, + SOMFY_MYLINK_COMPONENTS, +) + +CONFIG_OPTIONS = (CONF_DEFAULT_REVERSE, CONF_ENTITY_CONFIG) +UNDO_UPDATE_LISTENER = "undo_update_listener" + +_LOGGER = logging.getLogger(__name__) def validate_entity_config(values): @@ -29,17 +46,22 @@ def validate_entity_config(values): CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_SYSTEM_ID): cv.string, - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=44100): cv.port, - vol.Optional(CONF_DEFAULT_REVERSE, default=False): cv.boolean, - vol.Optional(CONF_ENTITY_CONFIG, default={}): validate_entity_config, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_SYSTEM_ID): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_DEFAULT_REVERSE, default=False): cv.boolean, + vol.Optional( + CONF_ENTITY_CONFIG, default={} + ): validate_entity_config, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) @@ -47,15 +69,125 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): """Set up the MyLink platform.""" - host = config[DOMAIN][CONF_HOST] - port = config[DOMAIN][CONF_PORT] - system_id = config[DOMAIN][CONF_SYSTEM_ID] - entity_config = config[DOMAIN][CONF_ENTITY_CONFIG] - entity_config[CONF_DEFAULT_REVERSE] = config[DOMAIN][CONF_DEFAULT_REVERSE] - somfy_mylink = SomfyMyLinkSynergy(system_id, host, port) - hass.data[DATA_SOMFY_MYLINK] = somfy_mylink + conf = config.get(DOMAIN) + hass.data.setdefault(DOMAIN, {}) + + if not conf: + return True + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf + ) + ) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Somfy MyLink from a config entry.""" + _async_import_options_from_data_if_missing(hass, entry) + + config = entry.data + somfy_mylink = SomfyMyLinkSynergy( + config[CONF_SYSTEM_ID], config[CONF_HOST], config[CONF_PORT] + ) + + try: + mylink_status = await somfy_mylink.status_info() + except asyncio.TimeoutError as ex: + raise ConfigEntryNotReady( + "Unable to connect to the Somfy MyLink device, please check your settings" + ) from ex + + if not mylink_status or "error" in mylink_status: + _LOGGER.error( + "mylink failed to setup because of an error: %s", + mylink_status.get("error", {}).get( + "message", "Empty response from mylink device" + ), + ) + return False + + _async_migrate_entity_config(hass, entry, mylink_status) + + undo_listener = entry.add_update_listener(_async_update_listener) + + hass.data[DOMAIN][entry.entry_id] = { + DATA_SOMFY_MYLINK: somfy_mylink, + MYLINK_STATUS: mylink_status, + UNDO_UPDATE_LISTENER: undo_listener, + } + for component in SOMFY_MYLINK_COMPONENTS: hass.async_create_task( - async_load_platform(hass, component, DOMAIN, entity_config, config) + hass.config_entries.async_forward_entry_setup(entry, component) ) + return True + + +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + +@callback +def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: ConfigEntry): + options = dict(entry.options) + data = dict(entry.data) + modified = False + + for importable_option in CONFIG_OPTIONS: + if importable_option not in options and importable_option in data: + options[importable_option] = data.pop(importable_option) + modified = True + + if modified: + hass.config_entries.async_update_entry(entry, data=data, options=options) + + +@callback +def _async_migrate_entity_config( + hass: HomeAssistant, entry: ConfigEntry, mylink_status: dict +): + if CONF_ENTITY_CONFIG not in entry.options: + return + + options = dict(entry.options) + + reversed_target_ids = options[CONF_REVERSED_TARGET_IDS] = {} + legacy_entry_config = options[CONF_ENTITY_CONFIG] + default_reverse = options.get(CONF_DEFAULT_REVERSE) + + for cover in mylink_status["result"]: + legacy_entity_id = ENTITY_ID_FORMAT.format(slugify(cover["name"])) + target_id = cover["targetID"] + + entity_config = legacy_entry_config.get(legacy_entity_id, {}) + if entity_config.get(CONF_REVERSE, default_reverse): + reversed_target_ids[target_id] = True + + for legacy_key in (CONF_DEFAULT_REVERSE, CONF_ENTITY_CONFIG): + if legacy_key in options: + del options[legacy_key] + + hass.config_entries.async_update_entry(entry, data=entry.data, options=options) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in SOMFY_MYLINK_COMPONENTS + ] + ) + ) + + hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/somfy_mylink/config_flow.py b/homeassistant/components/somfy_mylink/config_flow.py new file mode 100644 index 00000000000..ce69d265b55 --- /dev/null +++ b/homeassistant/components/somfy_mylink/config_flow.py @@ -0,0 +1,214 @@ +"""Config flow for Somfy MyLink integration.""" +import asyncio +from copy import deepcopy +import logging + +from somfy_mylink_synergy import SomfyMyLinkSynergy +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import callback +from homeassistant.helpers.device_registry import format_mac + +from .const import ( + CONF_REVERSE, + CONF_REVERSED_TARGET_IDS, + CONF_SYSTEM_ID, + CONF_TARGET_ID, + CONF_TARGET_NAME, + DEFAULT_PORT, + MYLINK_STATUS, +) +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from schema with values provided by the user. + """ + somfy_mylink = SomfyMyLinkSynergy( + data[CONF_SYSTEM_ID], data[CONF_HOST], data[CONF_PORT] + ) + + try: + status_info = await somfy_mylink.status_info() + except asyncio.TimeoutError as ex: + raise CannotConnect from ex + + if not status_info or "error" in status_info: + _LOGGER.debug("Auth error: %s", status_info) + raise InvalidAuth + + return {"title": f"MyLink {data[CONF_HOST]}"} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Somfy MyLink.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_ASSUMED + + def __init__(self): + """Initialize the somfy_mylink flow.""" + self.host = None + self.mac = None + self.ip_address = None + + async def async_step_dhcp(self, dhcp_discovery): + """Handle dhcp discovery.""" + if self._host_already_configured(dhcp_discovery[IP_ADDRESS]): + return self.async_abort(reason="already_configured") + + formatted_mac = format_mac(dhcp_discovery[MAC_ADDRESS]) + await self.async_set_unique_id(format_mac(formatted_mac)) + self._abort_if_unique_id_configured( + updates={CONF_HOST: dhcp_discovery[IP_ADDRESS]} + ) + self.host = dhcp_discovery[HOSTNAME] + self.mac = formatted_mac + self.ip_address = dhcp_discovery[IP_ADDRESS] + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context["title_placeholders"] = {"ip": self.ip_address, "mac": self.mac} + return await self.async_step_user() + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + + if user_input is not None: + if self._host_already_configured(user_input[CONF_HOST]): + return self.async_abort(reason="already_configured") + + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST, default=self.ip_address): str, + vol.Required(CONF_SYSTEM_ID): str, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, + } + ), + errors=errors, + ) + + async def async_step_import(self, user_input): + """Handle import.""" + if self._host_already_configured(user_input[CONF_HOST]): + return self.async_abort(reason="already_configured") + + return await self.async_step_user(user_input) + + def _host_already_configured(self, host): + """See if we already have an entry matching the host.""" + for entry in self._async_current_entries(): + if entry.data.get(CONF_HOST) == host: + return True + return False + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for somfy_mylink.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """Initialize options flow.""" + self.config_entry = config_entry + self.options = deepcopy(dict(config_entry.options)) + self._target_id = None + + @callback + def _async_callback_targets(self): + """Return the list of targets.""" + return self.hass.data[DOMAIN][self.config_entry.entry_id][MYLINK_STATUS][ + "result" + ] + + @callback + def _async_get_target_name(self, target_id) -> str: + """Find the name of a target in the api data.""" + mylink_targets = self._async_callback_targets() + for cover in mylink_targets: + if cover["targetID"] == target_id: + return cover["name"] + raise KeyError + + async def async_step_init(self, user_input=None): + """Handle options flow.""" + + if self.config_entry.state != config_entries.ENTRY_STATE_LOADED: + _LOGGER.error("MyLink must be connected to manage device options") + return self.async_abort(reason="cannot_connect") + + if user_input is not None: + target_id = user_input.get(CONF_TARGET_ID) + if target_id: + return await self.async_step_target_config(None, target_id) + + return self.async_create_entry(title="", data=self.options) + + cover_dict = {None: None} + mylink_targets = self._async_callback_targets() + if mylink_targets: + for cover in mylink_targets: + cover_dict[cover["targetID"]] = cover["name"] + + data_schema = vol.Schema({vol.Optional(CONF_TARGET_ID): vol.In(cover_dict)}) + + return self.async_show_form(step_id="init", data_schema=data_schema, errors={}) + + async def async_step_target_config(self, user_input=None, target_id=None): + """Handle options flow for target.""" + reversed_target_ids = self.options.setdefault(CONF_REVERSED_TARGET_IDS, {}) + + if user_input is not None: + if user_input[CONF_REVERSE] != reversed_target_ids.get(self._target_id): + reversed_target_ids[self._target_id] = user_input[CONF_REVERSE] + return await self.async_step_init() + + self._target_id = target_id + + return self.async_show_form( + step_id="target_config", + data_schema=vol.Schema( + { + vol.Optional( + CONF_REVERSE, + default=reversed_target_ids.get(target_id, False), + ): bool + } + ), + description_placeholders={ + CONF_TARGET_NAME: self._async_get_target_name(target_id), + }, + errors={}, + ) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/somfy_mylink/const.py b/homeassistant/components/somfy_mylink/const.py new file mode 100644 index 00000000000..a7cbf864cd9 --- /dev/null +++ b/homeassistant/components/somfy_mylink/const.py @@ -0,0 +1,19 @@ +"""Component for the Somfy MyLink device supporting the Synergy API.""" + +CONF_ENTITY_CONFIG = "entity_config" +CONF_SYSTEM_ID = "system_id" +CONF_REVERSE = "reverse" +CONF_DEFAULT_REVERSE = "default_reverse" +CONF_TARGET_NAME = "target_name" +CONF_REVERSED_TARGET_IDS = "reversed_target_ids" +CONF_TARGET_ID = "target_id" + +DEFAULT_PORT = 44100 + +DATA_SOMFY_MYLINK = "somfy_mylink_data" +MYLINK_STATUS = "mylink_status" +DOMAIN = "somfy_mylink" + +SOMFY_MYLINK_COMPONENTS = ["cover"] + +MANUFACTURER = "Somfy" diff --git a/homeassistant/components/somfy_mylink/cover.py b/homeassistant/components/somfy_mylink/cover.py index ac3bf0673f1..2725e2da9c7 100644 --- a/homeassistant/components/somfy_mylink/cover.py +++ b/homeassistant/components/somfy_mylink/cover.py @@ -2,49 +2,58 @@ import logging from homeassistant.components.cover import ( + DEVICE_CLASS_BLIND, + DEVICE_CLASS_SHUTTER, DEVICE_CLASS_WINDOW, - ENTITY_ID_FORMAT, CoverEntity, ) -from homeassistant.util import slugify +from homeassistant.const import STATE_CLOSED, STATE_OPEN +from homeassistant.helpers.restore_state import RestoreEntity -from . import CONF_DEFAULT_REVERSE, DATA_SOMFY_MYLINK +from .const import ( + CONF_REVERSED_TARGET_IDS, + DATA_SOMFY_MYLINK, + DOMAIN, + MANUFACTURER, + MYLINK_STATUS, +) _LOGGER = logging.getLogger(__name__) +MYLINK_COVER_TYPE_TO_DEVICE_CLASS = {0: DEVICE_CLASS_BLIND, 1: DEVICE_CLASS_SHUTTER} -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + +async def async_setup_entry(hass, config_entry, async_add_entities): """Discover and configure Somfy covers.""" - if discovery_info is None: - return - somfy_mylink = hass.data[DATA_SOMFY_MYLINK] + reversed_target_ids = config_entry.options.get(CONF_REVERSED_TARGET_IDS, {}) + + data = hass.data[DOMAIN][config_entry.entry_id] + mylink_status = data[MYLINK_STATUS] + somfy_mylink = data[DATA_SOMFY_MYLINK] cover_list = [] - try: - mylink_status = await somfy_mylink.status_info() - except TimeoutError: - _LOGGER.error( - "Unable to connect to the Somfy MyLink device, " - "please check your settings" - ) - return + for cover in mylink_status["result"]: - entity_id = ENTITY_ID_FORMAT.format(slugify(cover["name"])) - entity_config = discovery_info.get(entity_id, {}) - default_reverse = discovery_info[CONF_DEFAULT_REVERSE] - cover_config = {} - cover_config["target_id"] = cover["targetID"] - cover_config["name"] = cover["name"] - cover_config["reverse"] = entity_config.get("reverse", default_reverse) + cover_config = { + "target_id": cover["targetID"], + "name": cover["name"], + "device_class": MYLINK_COVER_TYPE_TO_DEVICE_CLASS.get( + cover.get("type"), DEVICE_CLASS_WINDOW + ), + "reverse": reversed_target_ids.get(cover["targetID"], False), + } + cover_list.append(SomfyShade(somfy_mylink, **cover_config)) + _LOGGER.info( "Adding Somfy Cover: %s with targetID %s", cover_config["name"], cover_config["target_id"], ) + async_add_entities(cover_list) -class SomfyShade(CoverEntity): +class SomfyShade(RestoreEntity, CoverEntity): """Object for controlling a Somfy cover.""" def __init__( @@ -60,8 +69,16 @@ class SomfyShade(CoverEntity): self._target_id = target_id self._name = name self._reverse = reverse + self._closed = None + self._is_opening = None + self._is_closing = None self._device_class = device_class + @property + def should_poll(self): + """No polling since assumed state.""" + return False + @property def unique_id(self): """Return the unique ID of this cover.""" @@ -72,11 +89,6 @@ class SomfyShade(CoverEntity): """Return the name of the cover.""" return self._name - @property - def is_closed(self): - """Return if the cover is closed.""" - return None - @property def assumed_state(self): """Let HA know the integration is assumed state.""" @@ -87,20 +99,72 @@ class SomfyShade(CoverEntity): """Return the class of this device, from component DEVICE_CLASSES.""" return self._device_class - async def async_open_cover(self, **kwargs): - """Wrap Homeassistant calls to open the cover.""" - if not self._reverse: - await self.somfy_mylink.move_up(self._target_id) - else: - await self.somfy_mylink.move_down(self._target_id) + @property + def is_opening(self): + """Return if the cover is opening.""" + return self._is_opening + + @property + def is_closing(self): + """Return if the cover is closing.""" + return self._is_closing + + @property + def is_closed(self) -> bool: + """Return if the cover is closed.""" + return self._closed + + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self._target_id)}, + "name": self._name, + "manufacturer": MANUFACTURER, + } async def async_close_cover(self, **kwargs): - """Wrap Homeassistant calls to close the cover.""" - if not self._reverse: - await self.somfy_mylink.move_down(self._target_id) - else: - await self.somfy_mylink.move_up(self._target_id) + """Close the cover.""" + self._is_closing = True + self.async_write_ha_state() + try: + # Blocks until the close command is sent + if not self._reverse: + await self.somfy_mylink.move_down(self._target_id) + else: + await self.somfy_mylink.move_up(self._target_id) + self._closed = True + finally: + self._is_closing = None + self.async_write_ha_state() + + async def async_open_cover(self, **kwargs): + """Open the cover.""" + self._is_opening = True + self.async_write_ha_state() + try: + # Blocks until the open command is sent + if not self._reverse: + await self.somfy_mylink.move_up(self._target_id) + else: + await self.somfy_mylink.move_down(self._target_id) + self._closed = False + finally: + self._is_opening = None + self.async_write_ha_state() async def async_stop_cover(self, **kwargs): """Stop the cover.""" await self.somfy_mylink.move_stop(self._target_id) + + async def async_added_to_hass(self): + """Complete the initialization.""" + await super().async_added_to_hass() + # Restore the last state + last_state = await self.async_get_last_state() + + if last_state is not None and last_state.state in ( + STATE_OPEN, + STATE_CLOSED, + ): + self._closed = last_state.state == STATE_CLOSED diff --git a/homeassistant/components/somfy_mylink/manifest.json b/homeassistant/components/somfy_mylink/manifest.json index c259f827d51..a7be33583d2 100644 --- a/homeassistant/components/somfy_mylink/manifest.json +++ b/homeassistant/components/somfy_mylink/manifest.json @@ -2,6 +2,12 @@ "domain": "somfy_mylink", "name": "Somfy MyLink", "documentation": "https://www.home-assistant.io/integrations/somfy_mylink", - "requirements": ["somfy-mylink-synergy==1.0.6"], - "codeowners": [] + "requirements": [ + "somfy-mylink-synergy==1.0.6" + ], + "codeowners": ["@bdraco"], + "config_flow": true, + "dhcp": [{ + "hostname":"somfy_*", "macaddress":"B8B7F1*" + }] } diff --git a/homeassistant/components/somfy_mylink/strings.json b/homeassistant/components/somfy_mylink/strings.json new file mode 100644 index 00000000000..ca3d83e402b --- /dev/null +++ b/homeassistant/components/somfy_mylink/strings.json @@ -0,0 +1,44 @@ +{ + "title": "Somfy MyLink", + "config": { + "flow_title": "Somfy MyLink {mac} ({ip})", + "step": { + "user": { + "description": "The System ID can be obtained in the MyLink app under Integration by selecting any non-Cloud service.", + "data": { + "host": "[%key:common::config_flow::data::host%]", + "port": "[%key:common::config_flow::data::port%]", + "system_id": "System ID" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "options": { + "abort": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "step": { + "init": { + "title": "Configure MyLink Options", + "data": { + "target_id": "Configure options for a cover." + } + }, + "target_config": { + "title": "Configure MyLink Cover", + "description": "Configure options for `{target_name}`", + "data": { + "reverse": "Cover is reversed" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/ca.json b/homeassistant/components/somfy_mylink/translations/ca.json new file mode 100644 index 00000000000..93ae58ca2bf --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/ca.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "flow_title": "Somfy MyLink {mac} ({ip})", + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port", + "system_id": "ID del sistema" + }, + "description": "L'ID de sistema es pot obtenir des de l'aplicaci\u00f3 MyLink dins de Integraci\u00f3, seleccionant qualsevol servei que no sigui al n\u00favol." + } + } + }, + "options": { + "abort": { + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "step": { + "entity_config": { + "data": { + "reverse": "La coberta est\u00e0 invertida" + }, + "description": "Opcions de configuraci\u00f3 de `{entity_id}`", + "title": "Configura l'entitat" + }, + "init": { + "data": { + "default_reverse": "Estat d'inversi\u00f3 predeterminat per a cobertes sense configurar", + "entity_id": "Configura una entitat espec\u00edfica.", + "target_id": "Opcions de configuraci\u00f3 de la coberta." + }, + "title": "Configura opcions de MyLink" + }, + "target_config": { + "data": { + "reverse": "La coberta est\u00e0 invertida" + }, + "description": "Opcions de configuraci\u00f3 de `{target_name}`", + "title": "Configura coberta MyLink" + } + } + }, + "title": "Somfy MyLink" +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/cs.json b/homeassistant/components/somfy_mylink/translations/cs.json new file mode 100644 index 00000000000..71e05b51544 --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/cs.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "invalid_auth": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "flow_title": "Somfy MyLink {mac} ({ip})", + "step": { + "user": { + "data": { + "host": "Hostitel", + "port": "Port" + } + } + } + }, + "options": { + "abort": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/de.json b/homeassistant/components/somfy_mylink/translations/de.json new file mode 100644 index 00000000000..522e185af5d --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/de.json @@ -0,0 +1,39 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "flow_title": "Somfy MyLink {mac} ({ip})", + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port", + "system_id": "System-ID" + } + } + } + }, + "options": { + "abort": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "step": { + "entity_config": { + "title": "Entit\u00e4t konfigurieren" + }, + "init": { + "title": "MyLink-Optionen konfigurieren" + }, + "target_config": { + "description": "Konfiguriere die Optionen f\u00fcr `{target_name}`", + "title": "MyLink-Cover konfigurieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/en.json b/homeassistant/components/somfy_mylink/translations/en.json new file mode 100644 index 00000000000..13115b36e5c --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/en.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "flow_title": "Somfy MyLink {mac} ({ip})", + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port", + "system_id": "System ID" + }, + "description": "The System ID can be obtained in the MyLink app under Integration by selecting any non-Cloud service." + } + } + }, + "options": { + "abort": { + "cannot_connect": "Failed to connect" + }, + "step": { + "entity_config": { + "data": { + "reverse": "Cover is reversed" + }, + "description": "Configure options for `{entity_id}`", + "title": "Configure Entity" + }, + "init": { + "data": { + "default_reverse": "Default reversal status for unconfigured covers", + "entity_id": "Configure a specific entity.", + "target_id": "Configure options for a cover." + }, + "title": "Configure MyLink Options" + }, + "target_config": { + "data": { + "reverse": "Cover is reversed" + }, + "description": "Configure options for `{target_name}`", + "title": "Configure MyLink Cover" + } + } + }, + "title": "Somfy MyLink" +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/es.json b/homeassistant/components/somfy_mylink/translations/es.json new file mode 100644 index 00000000000..40d82a4522a --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/es.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "flow_title": "Somfy MyLink {mac} ({ip})", + "step": { + "user": { + "data": { + "host": "Host", + "port": "Puerto", + "system_id": "ID del sistema" + }, + "description": "El ID del sistema se puede obtener en la aplicaci\u00f3n MyLink en Integraci\u00f3n seleccionando cualquier servicio que no sea de la nube." + } + } + }, + "options": { + "abort": { + "cannot_connect": "No se pudo conectar" + }, + "step": { + "entity_config": { + "data": { + "reverse": "La cubierta est\u00e1 invertida" + }, + "description": "Configurar opciones para `{entity_id}`", + "title": "Configurar entidad" + }, + "init": { + "data": { + "default_reverse": "Estado de inversi\u00f3n predeterminado para cubiertas no configuradas", + "entity_id": "Configurar una entidad espec\u00edfica.", + "target_id": "Configurar opciones para una cubierta." + }, + "title": "Configurar opciones de MyLink" + }, + "target_config": { + "data": { + "reverse": "La cubierta est\u00e1 invertida" + }, + "description": "Configurar opciones para `{target_name}`", + "title": "Configurar la cubierta MyLink" + } + } + }, + "title": "Somfy MyLink" +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/et.json b/homeassistant/components/somfy_mylink/translations/et.json new file mode 100644 index 00000000000..6d965220d7e --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/et.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Vigane autentimine", + "unknown": "Ootamatu t\u00f5rge" + }, + "flow_title": "Somfy MyLink {mac} ( {ip} )", + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port", + "system_id": "S\u00fcsteemi ID" + }, + "description": "S\u00fcsteemi ID saab rakenduse MyLink sidumise alt valides mis tahes mitte- pilveteenuse." + } + } + }, + "options": { + "abort": { + "cannot_connect": "\u00dchendamine nurjus" + }, + "step": { + "entity_config": { + "data": { + "reverse": "(Akna)kate t\u00f6\u00f6tab vastupidi" + }, + "description": "Olemi {entity_id} suvandite seadmine", + "title": "Seadista olem" + }, + "init": { + "data": { + "default_reverse": "Seadistamata (akna)katete vaikep\u00f6\u00f6rduse olek", + "entity_id": "Seadista konkreetne olem.", + "target_id": "Seadista (akna)katte suvandid" + }, + "title": "Seadista MyLinki suvandid" + }, + "target_config": { + "data": { + "reverse": "(Akna)kate liigub vastupidi" + }, + "description": "Seadme `{target_name}` suvandite seadmine", + "title": "Seadista MyLink Cover" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/fr.json b/homeassistant/components/somfy_mylink/translations/fr.json new file mode 100644 index 00000000000..96904b6038d --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/fr.json @@ -0,0 +1,43 @@ +{ + "config": { + "error": { + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te" + } + } + } + }, + "options": { + "abort": { + "cannot_connect": "Echec de connection" + }, + "step": { + "entity_config": { + "data": { + "reverse": "La couverture est invers\u00e9e" + }, + "title": "Configurez une entit\u00e9 sp\u00e9cifique" + }, + "init": { + "data": { + "default_reverse": "Statut d'inversion par d\u00e9faut pour les couvertures non configur\u00e9es", + "entity_id": "Configurez une entit\u00e9 sp\u00e9cifique.", + "target_id": "Configurez les options pour la couverture." + }, + "title": "Configurer les options MyLink" + }, + "target_config": { + "data": { + "reverse": "La couverture est invers\u00e9e" + }, + "description": "Configurer les options pour \u00ab {target_name} \u00bb", + "title": "Configurer la couverture MyLink" + } + } + }, + "title": "Somfy MyLink" +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/it.json b/homeassistant/components/somfy_mylink/translations/it.json new file mode 100644 index 00000000000..ce049782c43 --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/it.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "flow_title": "Somfy MyLink {mac} ({ip})", + "step": { + "user": { + "data": { + "host": "Host", + "port": "Porta", + "system_id": "ID sistema" + }, + "description": "L'ID sistema pu\u00f2 essere ottenuto nell'app MyLink alla voce Integrazione selezionando qualsiasi servizio non-Cloud." + } + } + }, + "options": { + "abort": { + "cannot_connect": "Impossibile connettersi" + }, + "step": { + "entity_config": { + "data": { + "reverse": "La tapparella \u00e8 invertita" + }, + "description": "Configura le opzioni per `{entity_id}`", + "title": "Configura entit\u00e0" + }, + "init": { + "data": { + "default_reverse": "Stato d'inversione predefinito per le tapparelle non configurate", + "entity_id": "Configura un'entit\u00e0 specifica.", + "target_id": "Configura opzioni per una tapparella" + }, + "title": "Configura le opzioni MyLink" + }, + "target_config": { + "data": { + "reverse": "La tapparella \u00e8 invertita" + }, + "description": "Configura le opzioni per `{target_name}`", + "title": "Configura tapparelle MyLink" + } + } + }, + "title": "Somfy MyLink" +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/lb.json b/homeassistant/components/somfy_mylink/translations/lb.json new file mode 100644 index 00000000000..efaba3ab497 --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/lb.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port", + "system_id": "System ID" + } + } + } + }, + "options": { + "abort": { + "cannot_connect": "Feeler beim verbannen" + } + }, + "title": "Somfy MyLink" +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/no.json b/homeassistant/components/somfy_mylink/translations/no.json new file mode 100644 index 00000000000..5b9b6608c25 --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/no.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "flow_title": "", + "step": { + "user": { + "data": { + "host": "Vert", + "port": "Port", + "system_id": "" + }, + "description": "System-ID-en kan f\u00e5s i MyLink-appen under Integrasjon ved \u00e5 velge en hvilken som helst ikke-Cloud-tjeneste." + } + } + }, + "options": { + "abort": { + "cannot_connect": "Tilkobling mislyktes" + }, + "step": { + "entity_config": { + "data": { + "reverse": "Rullegardinet reverseres" + }, + "description": "Konfigurer alternativer for \"{entity_id}\"", + "title": "Konfigurer enhet" + }, + "init": { + "data": { + "default_reverse": "Standard tilbakef\u00f8ringsstatus for ukonfigurerte rullegardiner", + "entity_id": "Konfigurer en bestemt enhet.", + "target_id": "Konfigurer alternativer for et rullgardin" + }, + "title": "Konfigurere MyLink-alternativer" + }, + "target_config": { + "data": { + "reverse": "Rullegardinet reverseres" + }, + "description": "Konfigurer alternativer for \"{target_name}\"", + "title": "Konfigurer MyLink-deksel" + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/pl.json b/homeassistant/components/somfy_mylink/translations/pl.json new file mode 100644 index 00000000000..7e49ecb2bca --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/pl.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_auth": "Niepoprawne uwierzytelnienie", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "flow_title": "Somfy MyLink {mac} ({ip})", + "step": { + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "port": "Port", + "system_id": "Identyfikator systemu" + }, + "description": "Identyfikator systemu mo\u017cna uzyska\u0107 w aplikacji MyLink w sekcji Integracja, wybieraj\u0105c dowoln\u0105 us\u0142ug\u0119 spoza chmury." + } + } + }, + "options": { + "abort": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "step": { + "entity_config": { + "data": { + "reverse": "Roleta/pokrywa jest odwr\u00f3cona" + }, + "description": "Konfiguracja opcji dla \"{entity_id}\"", + "title": "Konfigurowanie encji" + }, + "init": { + "data": { + "default_reverse": "Domy\u015blny stan odwr\u00f3cenia nieskonfigurowanych rolet/pokryw", + "entity_id": "Skonfiguruj okre\u015blon\u0105 encj\u0119.", + "target_id": "Konfiguracja opcji rolety" + }, + "title": "Konfiguracja opcji MyLink" + }, + "target_config": { + "data": { + "reverse": "Roleta/pokrywa jest odwr\u00f3cona" + }, + "description": "Konfiguracja opcji dla \"{target_name}\"", + "title": "Konfiguracja rolety MyLink" + } + } + }, + "title": "Somfy MyLink" +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/ru.json b/homeassistant/components/somfy_mylink/translations/ru.json new file mode 100644 index 00000000000..e4cc7b71712 --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/ru.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "Somfy MyLink {mac} ({ip})", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442", + "system_id": "System ID" + }, + "description": "System ID \u043c\u043e\u0436\u043d\u043e \u0443\u0437\u043d\u0430\u0442\u044c \u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0438 MyLink \u0432 \u0440\u0430\u0437\u0434\u0435\u043b\u0435 \u00ab\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f\u00bb, \u0432\u044b\u0431\u0440\u0430\u0432 \u043b\u044e\u0431\u0443\u044e \u043d\u0435 \u043e\u0431\u043b\u0430\u0447\u043d\u0443\u044e \u0441\u043b\u0443\u0436\u0431\u0443." + } + } + }, + "options": { + "abort": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "step": { + "entity_config": { + "data": { + "reverse": "\u0418\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0434\u043b\u044f \u0448\u0442\u043e\u0440 \u0438 \u0436\u0430\u043b\u044e\u0437\u0438" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u0434\u043b\u044f `{entity_id}`", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043e\u0431\u044a\u0435\u043a\u0442\u0430" + }, + "init": { + "data": { + "default_reverse": "\u041f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0438\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0434\u043b\u044f \u0448\u0442\u043e\u0440 \u0438 \u0436\u0430\u043b\u044e\u0437\u0438", + "entity_id": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043d\u043a\u0440\u0435\u0442\u043d\u043e\u0433\u043e \u043e\u0431\u044a\u0435\u043a\u0442\u0430", + "target_id": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u0448\u0442\u043e\u0440 \u0438\u043b\u0438 \u0436\u0430\u043b\u044e\u0437\u0438." + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 MyLink" + }, + "target_config": { + "data": { + "reverse": "\u0418\u043d\u0432\u0435\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 \u0434\u043b\u044f \u0448\u0442\u043e\u0440 \u0438 \u0436\u0430\u043b\u044e\u0437\u0438" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 \u0434\u043b\u044f `{target_name}`", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 MyLink Cover" + } + } + }, + "title": "Somfy MyLink" +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/tr.json b/homeassistant/components/somfy_mylink/translations/tr.json new file mode 100644 index 00000000000..29530b65659 --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/tr.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "flow_title": "Somfy MyLink {mac} ( {ip} )", + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port", + "system_id": "Sistem ID" + }, + "description": "Sistem Kimli\u011fi, MyLink uygulamas\u0131nda Entegrasyon alt\u0131nda Bulut d\u0131\u015f\u0131 herhangi bir hizmet se\u00e7ilerek elde edilebilir." + } + } + }, + "options": { + "abort": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "entity_config": { + "data": { + "reverse": "Kapak ters \u00e7evrildi" + }, + "description": "'{entity_id}' i\u00e7in se\u00e7enekleri yap\u0131land\u0131r\u0131n", + "title": "Varl\u0131\u011f\u0131 Yap\u0131land\u0131r" + }, + "init": { + "data": { + "default_reverse": "Yap\u0131land\u0131r\u0131lmam\u0131\u015f kapaklar i\u00e7in varsay\u0131lan geri alma durumu", + "entity_id": "Belirli bir varl\u0131\u011f\u0131 yap\u0131land\u0131r\u0131n.", + "target_id": "Kapak i\u00e7in se\u00e7enekleri yap\u0131land\u0131r\u0131n." + }, + "title": "MyLink Se\u00e7eneklerini Yap\u0131land\u0131r\u0131n" + }, + "target_config": { + "data": { + "reverse": "Kapak ters \u00e7evrildi" + }, + "description": "'{target_name}' i\u00e7in se\u00e7enekleri yap\u0131land\u0131r\u0131n", + "title": "MyLink Kapa\u011f\u0131n\u0131 Yap\u0131land\u0131r\u0131n" + } + } + }, + "title": "Somfy MyLink" +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/uk.json b/homeassistant/components/somfy_mylink/translations/uk.json new file mode 100644 index 00000000000..2d251531340 --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/uk.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442", + "system_id": "System ID" + }, + "description": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0441\u0438\u0441\u0442\u0435\u043c\u0438 \u043c\u043e\u0436\u043d\u0430 \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0432 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0456 MyLink \u0443 \u0440\u043e\u0437\u0434\u0456\u043b\u0456 \u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f, \u0432\u0438\u0431\u0440\u0430\u0432\u0448\u0438 \u0431\u0443\u0434\u044c-\u044f\u043a\u0443 \u043d\u0435\u0445\u043c\u0430\u0440\u043d\u0443 \u0441\u043b\u0443\u0436\u0431\u0443." + } + } + }, + "options": { + "abort": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "entity_config": { + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0456\u0432 \u0434\u043b\u044f \"{entity_id}\"", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u0441\u0443\u0442\u043d\u0456\u0441\u0442\u044c" + }, + "init": { + "data": { + "entity_id": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0441\u043f\u0435\u0446\u0438\u0444\u0456\u0447\u043d\u043e\u0457 \u0441\u0443\u0442\u043d\u043e\u0441\u0442\u0456." + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0441\u0443\u0442\u043d\u043e\u0441\u0442\u0435\u0439 MyLink" + } + } + }, + "title": "Somfy MyLink" +} \ No newline at end of file diff --git a/homeassistant/components/somfy_mylink/translations/zh-Hant.json b/homeassistant/components/somfy_mylink/translations/zh-Hant.json new file mode 100644 index 00000000000..2abb6a64f7c --- /dev/null +++ b/homeassistant/components/somfy_mylink/translations/zh-Hant.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "Somfy MyLink {mac} ({ip})", + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0", + "system_id": "\u7cfb\u7d71 ID" + }, + "description": "\u7cfb\u7d71 ID \u53ef\u4ee5\u65bc\u6574\u5408\u5167\u7684 MyLink app \u9078\u64c7\u975e\u96f2\u7aef\u670d\u52d9\u4e2d\u627e\u5230\u3002" + } + } + }, + "options": { + "abort": { + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "step": { + "entity_config": { + "data": { + "reverse": "\u7a97\u7c3e\u53cd\u5411" + }, + "description": "`{entity_id}` \u8a2d\u5b9a\u9078\u9805", + "title": "\u8a2d\u5b9a\u5be6\u9ad4" + }, + "init": { + "data": { + "default_reverse": "\u672a\u8a2d\u5b9a\u7a97\u7c3e\u9810\u8a2d\u70ba\u53cd\u5411", + "entity_id": "\u8a2d\u5b9a\u7279\u5b9a\u5be6\u9ad4\u3002", + "target_id": "\u7a97\u7c3e\u8a2d\u5b9a\u9078\u9805\u3002" + }, + "title": "MyLink \u8a2d\u5b9a\u9078\u9805" + }, + "target_config": { + "data": { + "reverse": "\u7a97\u7c3e\u53cd\u5411" + }, + "description": "`{target_name}` \u8a2d\u5b9a\u9078\u9805", + "title": "\u8a2d\u5b9a MyLink \u7a97\u7c3e" + } + } + }, + "title": "Somfy MyLink" +} \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/de.json b/homeassistant/components/sonarr/translations/de.json index 3abc6b45ef3..19a37dbcc4f 100644 --- a/homeassistant/components/sonarr/translations/de.json +++ b/homeassistant/components/sonarr/translations/de.json @@ -1,19 +1,27 @@ { "config": { "abort": { + "already_configured": "Der Dienst ist bereits konfiguriert", + "reauth_successful": "Die erneute Authentifizierung war erfolgreich", "unknown": "Unerwateter Fehler" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen" + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "flow_title": "Sonarr: {name}", "step": { + "reauth_confirm": { + "title": "Integration erneut authentifizieren" + }, "user": { "data": { "api_key": "API Schl\u00fcssel", "base_path": "Pfad zur API", "host": "Host", - "port": "Port" + "port": "Port", + "ssl": "Verwendet ein SSL-Zertifikat", + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" } } } diff --git a/homeassistant/components/sonarr/translations/tr.json b/homeassistant/components/sonarr/translations/tr.json new file mode 100644 index 00000000000..eadf0100045 --- /dev/null +++ b/homeassistant/components/sonarr/translations/tr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu", + "unknown": "Beklenmeyen hata" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "api_key": "API Anahtar\u0131", + "host": "Ana Bilgisayar", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sonarr/translations/uk.json b/homeassistant/components/sonarr/translations/uk.json new file mode 100644 index 00000000000..0b6b7acf26d --- /dev/null +++ b/homeassistant/components/sonarr/translations/uk.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u043f\u0440\u043e\u0439\u0448\u043b\u0430 \u0443\u0441\u043f\u0456\u0448\u043d\u043e", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "flow_title": "Sonarr: {name}", + "step": { + "reauth_confirm": { + "description": "\u041f\u043e\u0442\u0440\u0456\u0431\u043d\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e API Sonarr \u0437\u0430 \u0430\u0434\u0440\u0435\u0441\u043e\u044e: {host}", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0432\u0430\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e" + }, + "user": { + "data": { + "api_key": "\u041a\u043b\u044e\u0447 API", + "base_path": "\u0428\u043b\u044f\u0445 \u0434\u043e API", + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL", + "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "upcoming_days": "\u041a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u043c\u0430\u0439\u0431\u0443\u0442\u043d\u0456\u0445 \u0434\u043d\u0456\u0432 \u0434\u043b\u044f \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u043d\u044f", + "wanted_max_items": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430 \u043a\u0456\u043b\u044c\u043a\u0456\u0441\u0442\u044c \u0435\u043b\u0435\u043c\u0435\u043d\u0442\u0456\u0432 \u0434\u043b\u044f \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u043d\u044f" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/songpal/translations/tr.json b/homeassistant/components/songpal/translations/tr.json new file mode 100644 index 00000000000..ab90d4b1067 --- /dev/null +++ b/homeassistant/components/songpal/translations/tr.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "endpoint": "Biti\u015f noktas\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/songpal/translations/uk.json b/homeassistant/components/songpal/translations/uk.json new file mode 100644 index 00000000000..893077a826d --- /dev/null +++ b/homeassistant/components/songpal/translations/uk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "not_songpal_device": "\u0426\u0435 \u043d\u0435 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 Songpal." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "flow_title": "Sony Songpal {name} ({host})", + "step": { + "init": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name} ({host})?" + }, + "user": { + "data": { + "endpoint": "\u041a\u0456\u043d\u0446\u0435\u0432\u0430 \u0442\u043e\u0447\u043a\u0430" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/__init__.py b/homeassistant/components/sonos/__init__.py index cc33134c810..c3a977e32e1 100644 --- a/homeassistant/components/sonos/__init__.py +++ b/homeassistant/components/sonos/__init__.py @@ -4,11 +4,9 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import CONF_HOSTS -from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import config_validation as cv -from homeassistant.loader import bind_hass -from .const import DATA_SONOS, DOMAIN +from .const import DOMAIN CONF_ADVERTISE_ADDR = "advertise_addr" CONF_INTERFACE_ADDR = "interface_addr" @@ -55,23 +53,3 @@ async def async_setup_entry(hass, entry): hass.config_entries.async_forward_entry_setup(entry, MP_DOMAIN) ) return True - - -@bind_hass -def get_coordinator_name(hass, entity_id): - """Obtain the room/name of a device's coordinator. - - Used by the Plex integration. - - This function is safe to run inside the event loop. - """ - if DATA_SONOS not in hass.data: - raise HomeAssistantError("Sonos integration not set up") - - device = next( - (x for x in hass.data[DATA_SONOS].entities if x.entity_id == entity_id), None - ) - - if device.is_coordinator: - return device.name - return device.coordinator.name diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 66e6587b9ff..1852f9c3849 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -4,6 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", "requirements": ["pysonos==0.0.37"], + "after_dependencies": ["plex"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:ZonePlayer:1" diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 48b22256030..9d89bdf68f8 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -59,6 +59,8 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, ) from homeassistant.components.media_player.errors import BrowseError +from homeassistant.components.plex.const import PLEX_URI_SCHEME +from homeassistant.components.plex.services import play_on_sonos from homeassistant.const import ( ATTR_TIME, EVENT_HOMEASSISTANT_STOP, @@ -1186,12 +1188,17 @@ class SonosEntity(MediaPlayerEntity): """ Send the play_media command to the media player. + If media_id is a Plex payload, attempt Plex->Sonos playback. + If media_type is "playlist", media_id should be a Sonos Playlist name. Otherwise, media_id should be a URI. If ATTR_MEDIA_ENQUEUE is True, add `media_id` to the queue. """ - if media_type in (MEDIA_TYPE_MUSIC, MEDIA_TYPE_TRACK): + if media_id and media_id.startswith(PLEX_URI_SCHEME): + media_id = media_id[len(PLEX_URI_SCHEME) :] + play_on_sonos(self.hass, media_type, media_id, self.name) + elif media_type in (MEDIA_TYPE_MUSIC, MEDIA_TYPE_TRACK): if kwargs.get(ATTR_MEDIA_ENQUEUE): try: if self.soco.is_spotify_uri(media_id): diff --git a/homeassistant/components/sonos/translations/de.json b/homeassistant/components/sonos/translations/de.json index 93b25cf0b97..5d66c168116 100644 --- a/homeassistant/components/sonos/translations/de.json +++ b/homeassistant/components/sonos/translations/de.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Keine Sonos Ger\u00e4te im Netzwerk gefunden.", - "single_instance_allowed": "Nur eine einzige Konfiguration von Sonos ist notwendig." + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "step": { "confirm": { diff --git a/homeassistant/components/sonos/translations/tr.json b/homeassistant/components/sonos/translations/tr.json new file mode 100644 index 00000000000..42bd46ce7c0 --- /dev/null +++ b/homeassistant/components/sonos/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "confirm": { + "description": "Sonos'u kurmak istiyor musunuz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sonos/translations/uk.json b/homeassistant/components/sonos/translations/uk.json new file mode 100644 index 00000000000..aff6c9f59b1 --- /dev/null +++ b/homeassistant/components/sonos/translations/uk.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "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." + }, + "step": { + "confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Sonos?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/speedtestdotnet/translations/de.json b/homeassistant/components/speedtestdotnet/translations/de.json index 56b1a91a89e..3b5ef0b26e1 100644 --- a/homeassistant/components/speedtestdotnet/translations/de.json +++ b/homeassistant/components/speedtestdotnet/translations/de.json @@ -1,11 +1,12 @@ { "config": { "abort": { - "wrong_server_id": "Server ID ist ung\u00fcltig" + "single_instance_allowed": "Bereits konfiguriert. Es ist nur eine Konfiguration m\u00f6glich.", + "wrong_server_id": "Server-ID ist ung\u00fcltig" }, "step": { "user": { - "description": "Einrichtung beginnen?" + "description": "M\u00f6chtest du mit der Einrichtung beginnen?" } } }, @@ -13,7 +14,9 @@ "step": { "init": { "data": { - "manual": "Automatische Updates deaktivieren" + "manual": "Automatische Updates deaktivieren", + "scan_interval": "Aktualisierungsfrequenz (Minuten)", + "server_name": "Testserver ausw\u00e4hlen" } } } diff --git a/homeassistant/components/speedtestdotnet/translations/tr.json b/homeassistant/components/speedtestdotnet/translations/tr.json new file mode 100644 index 00000000000..b13be7c5e0c --- /dev/null +++ b/homeassistant/components/speedtestdotnet/translations/tr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "wrong_server_id": "Sunucu kimli\u011fi ge\u00e7erli de\u011fil" + }, + "step": { + "user": { + "description": "Kuruluma ba\u015flamak ister misiniz?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "manual": "Otomatik g\u00fcncellemeyi devre d\u0131\u015f\u0131 b\u0131rak\u0131n", + "scan_interval": "G\u00fcncelleme s\u0131kl\u0131\u011f\u0131 (dakika)", + "server_name": "Test sunucusunu se\u00e7in" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/speedtestdotnet/translations/uk.json b/homeassistant/components/speedtestdotnet/translations/uk.json new file mode 100644 index 00000000000..89ef24440d1 --- /dev/null +++ b/homeassistant/components/speedtestdotnet/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "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.", + "wrong_server_id": "\u041d\u0435\u043f\u0440\u0438\u043f\u0443\u0441\u0442\u0438\u043c\u0438\u0439 \u0456\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0441\u0435\u0440\u0432\u0435\u0440\u0430." + }, + "step": { + "user": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "manual": "\u0412\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0435 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f", + "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f (\u0443 \u0445\u0432\u0438\u043b\u0438\u043d\u0430\u0445)", + "server_name": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u0435\u0440\u0432\u0435\u0440 \u0434\u043b\u044f \u0442\u0435\u0441\u0442\u0443\u0432\u0430\u043d\u043d\u044f" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spider/translations/de.json b/homeassistant/components/spider/translations/de.json index 6f398062876..c57e55e9d2e 100644 --- a/homeassistant/components/spider/translations/de.json +++ b/homeassistant/components/spider/translations/de.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/spider/translations/tr.json b/homeassistant/components/spider/translations/tr.json new file mode 100644 index 00000000000..9bcc6bb1c41 --- /dev/null +++ b/homeassistant/components/spider/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spider/translations/uk.json b/homeassistant/components/spider/translations/uk.json new file mode 100644 index 00000000000..b8be2a14887 --- /dev/null +++ b/homeassistant/components/spider/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "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." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "\u0412\u0445\u0456\u0434 \u0432 \u0441\u0438\u0441\u0442\u0435\u043c\u0443 \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 mijn.ithodaalderop.nl" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/de.json b/homeassistant/components/spotify/translations/de.json index bfd393bbbb8..281803ec66e 100644 --- a/homeassistant/components/spotify/translations/de.json +++ b/homeassistant/components/spotify/translations/de.json @@ -2,14 +2,18 @@ "config": { "abort": { "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", - "missing_configuration": "Die Spotify-Integration ist nicht konfiguriert. Bitte folgen Sie der Dokumentation." + "missing_configuration": "Die Spotify-Integration ist nicht konfiguriert. Bitte folgen Sie der Dokumentation.", + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url})." }, "create_entry": { "default": "Erfolgreich mit Spotify authentifiziert." }, "step": { "pick_implementation": { - "title": "Authentifizierungsmethode ausw\u00e4hlen" + "title": "W\u00e4hle die Authentifizierungsmethode" + }, + "reauth_confirm": { + "title": "Integration erneut authentifizieren" } } }, diff --git a/homeassistant/components/spotify/translations/lb.json b/homeassistant/components/spotify/translations/lb.json index d7b5dcec0be..92e323d6c4d 100644 --- a/homeassistant/components/spotify/translations/lb.json +++ b/homeassistant/components/spotify/translations/lb.json @@ -17,5 +17,10 @@ "title": "Integratioun re-authentifiz\u00e9ieren" } } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "Spotify API Endpunkt ereechbar" + } } } \ No newline at end of file diff --git a/homeassistant/components/spotify/translations/uk.json b/homeassistant/components/spotify/translations/uk.json new file mode 100644 index 00000000000..fda84b310a5 --- /dev/null +++ b/homeassistant/components/spotify/translations/uk.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "missing_configuration": "\u0406\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044f Spotify \u043d\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0430. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044c \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0454\u044e.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.", + "reauth_account_mismatch": "\u0410\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u043e\u0432\u0430\u043d\u0438\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u043d\u0435 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u0430\u0454 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u043c\u0443 \u0437\u0430\u043f\u0438\u0441\u0443, \u0449\u043e \u0432\u0438\u043c\u0430\u0433\u0430\u0454 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e\u0457 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457." + }, + "create_entry": { + "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." + }, + "step": { + "pick_implementation": { + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + }, + "reauth_confirm": { + "description": "\u041d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u0430 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f \u0432 Spotify \u0434\u043b\u044f \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443: {account}", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0432\u0430\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e" + } + } + }, + "system_health": { + "info": { + "api_endpoint_reachable": "\u0414\u043e\u0441\u0442\u0443\u043f \u0434\u043e API Spotify" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index 459124820ff..3b21d32b110 100644 --- a/homeassistant/components/sql/manifest.json +++ b/homeassistant/components/sql/manifest.json @@ -2,6 +2,6 @@ "domain": "sql", "name": "SQL", "documentation": "https://www.home-assistant.io/integrations/sql", - "requirements": ["sqlalchemy==1.3.20"], + "requirements": ["sqlalchemy==1.3.22"], "codeowners": ["@dgomes"] } diff --git a/homeassistant/components/squeezebox/manifest.json b/homeassistant/components/squeezebox/manifest.json index 5c384e6ae1c..c31d80e1acf 100644 --- a/homeassistant/components/squeezebox/manifest.json +++ b/homeassistant/components/squeezebox/manifest.json @@ -8,5 +8,8 @@ "requirements": [ "pysqueezebox==0.5.5" ], - "config_flow": true + "config_flow": true, + "dhcp": [ + {"hostname":"squeezebox*","macaddress":"000420*"} + ] } diff --git a/homeassistant/components/squeezebox/translations/de.json b/homeassistant/components/squeezebox/translations/de.json index 667bf6dbd12..742210f3dc6 100644 --- a/homeassistant/components/squeezebox/translations/de.json +++ b/homeassistant/components/squeezebox/translations/de.json @@ -1,11 +1,17 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" }, "step": { "edit": { "data": { + "host": "Host", "password": "Passwort", "port": "Port", "username": "Benutzername" diff --git a/homeassistant/components/squeezebox/translations/tr.json b/homeassistant/components/squeezebox/translations/tr.json new file mode 100644 index 00000000000..ff249aafa14 --- /dev/null +++ b/homeassistant/components/squeezebox/translations/tr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "edit": { + "data": { + "host": "Ana Bilgisayar", + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + }, + "user": { + "data": { + "host": "Ana Bilgisayar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/squeezebox/translations/uk.json b/homeassistant/components/squeezebox/translations/uk.json new file mode 100644 index 00000000000..50cd135f6f3 --- /dev/null +++ b/homeassistant/components/squeezebox/translations/uk.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "no_server_found": "\u0421\u0435\u0440\u0432\u0435\u0440 LMS \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "no_server_found": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0432\u0438\u044f\u0432\u0438\u0442\u0438 \u0441\u0435\u0440\u0432\u0435\u0440.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "flow_title": "Logitech Squeezebox: {host}", + "step": { + "edit": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "\u0406\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044f \u043f\u0440\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/de.json b/homeassistant/components/srp_energy/translations/de.json index 23fe89c73b4..302233d2923 100644 --- a/homeassistant/components/srp_energy/translations/de.json +++ b/homeassistant/components/srp_energy/translations/de.json @@ -1,14 +1,21 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen" + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Anmeldung", + "unknown": "Unerwarteter Fehler" }, "step": { "user": { "data": { - "password": "Passwort" + "password": "Passwort", + "username": "Benutzername" } } } - } + }, + "title": "SRP Energy" } \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/es.json b/homeassistant/components/srp_energy/translations/es.json index de15bb80551..849c5019d3b 100644 --- a/homeassistant/components/srp_energy/translations/es.json +++ b/homeassistant/components/srp_energy/translations/es.json @@ -1,12 +1,12 @@ { "config": { "abort": { - "single_instance_allowed": "Ya configurado. Solo es posible una \u00fanica configuraci\u00f3n." + "single_instance_allowed": "Ya est\u00e1 configurado. Solo es posible una \u00fanica configuraci\u00f3n." }, "error": { "cannot_connect": "No se pudo conectar", "invalid_account": "El ID de la cuenta debe ser un n\u00famero de 9 d\u00edgitos", - "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "step": { @@ -15,7 +15,7 @@ "id": "ID de la cuenta", "is_tou": "Es el plan de tiempo de uso", "password": "Contrase\u00f1a", - "username": "Nombre de usuario" + "username": "Usuario" } } } diff --git a/homeassistant/components/srp_energy/translations/fr.json b/homeassistant/components/srp_energy/translations/fr.json new file mode 100644 index 00000000000..0cc85aff649 --- /dev/null +++ b/homeassistant/components/srp_energy/translations/fr.json @@ -0,0 +1,14 @@ +{ + "config": { + "error": { + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/lb.json b/homeassistant/components/srp_energy/translations/lb.json new file mode 100644 index 00000000000..1affdcc31e6 --- /dev/null +++ b/homeassistant/components/srp_energy/translations/lb.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech." + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "Benotzernumm" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/srp_energy/translations/tr.json b/homeassistant/components/srp_energy/translations/tr.json index 1b08426f631..ead8238d82c 100644 --- a/homeassistant/components/srp_energy/translations/tr.json +++ b/homeassistant/components/srp_energy/translations/tr.json @@ -1,7 +1,13 @@ { "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, "error": { - "invalid_account": "Hesap kimli\u011fi 9 haneli bir say\u0131 olmal\u0131d\u0131r" + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_account": "Hesap kimli\u011fi 9 haneli bir say\u0131 olmal\u0131d\u0131r", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" }, "step": { "user": { diff --git a/homeassistant/components/srp_energy/translations/uk.json b/homeassistant/components/srp_energy/translations/uk.json new file mode 100644 index 00000000000..5267aa2a575 --- /dev/null +++ b/homeassistant/components/srp_energy/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "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." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_account": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 \u043c\u0430\u0454 \u0431\u0443\u0442\u0438 9-\u0437\u043d\u0430\u0447\u043d\u0438\u043c \u043d\u043e\u043c\u0435\u0440\u043e\u043c.", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "id": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443", + "is_tou": "\u041f\u043b\u0430\u043d \u0447\u0430\u0441\u0443 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u043d\u043d\u044f", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + }, + "title": "SRP Energy" +} \ No newline at end of file diff --git a/homeassistant/components/ssdp/__init__.py b/homeassistant/components/ssdp/__init__.py index e962c141bef..f07e88d811a 100644 --- a/homeassistant/components/ssdp/__init__.py +++ b/homeassistant/components/ssdp/__init__.py @@ -171,13 +171,13 @@ class Scanner: session = self.hass.helpers.aiohttp_client.async_get_clientsession() try: resp = await session.get(xml_location, timeout=5) - xml = await resp.text() + xml = await resp.text(errors="replace") # Samsung Smart TV sometimes returns an empty document the # first time. Retry once. if not xml: resp = await session.get(xml_location, timeout=5) - xml = await resp.text() + xml = await resp.text(errors="replace") except (aiohttp.ClientError, asyncio.TimeoutError) as err: _LOGGER.debug("Error fetching %s: %s", xml_location, err) return {} diff --git a/homeassistant/components/starline/__init__.py b/homeassistant/components/starline/__init__.py index 303507b1491..392dbff9e03 100644 --- a/homeassistant/components/starline/__init__.py +++ b/homeassistant/components/starline/__init__.py @@ -8,10 +8,13 @@ from homeassistant.exceptions import ConfigEntryNotReady from .account import StarlineAccount from .const import ( CONF_SCAN_INTERVAL, + CONF_SCAN_OBD_INTERVAL, DEFAULT_SCAN_INTERVAL, + DEFAULT_SCAN_OBD_INTERVAL, DOMAIN, PLATFORMS, SERVICE_SET_SCAN_INTERVAL, + SERVICE_SET_SCAN_OBD_INTERVAL, SERVICE_UPDATE_STATE, ) @@ -25,6 +28,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b """Set up the StarLine device from a config entry.""" account = StarlineAccount(hass, config_entry) await account.update() + await account.update_obd() if not account.api.available: raise ConfigEntryNotReady @@ -44,12 +48,23 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ) async def async_set_scan_interval(call): - """Service for set scan interval.""" + """Set scan interval.""" options = dict(config_entry.options) options[CONF_SCAN_INTERVAL] = call.data[CONF_SCAN_INTERVAL] hass.config_entries.async_update_entry(entry=config_entry, options=options) - hass.services.async_register(DOMAIN, SERVICE_UPDATE_STATE, account.update) + async def async_set_scan_obd_interval(call): + """Set OBD info scan interval.""" + options = dict(config_entry.options) + options[CONF_SCAN_OBD_INTERVAL] = call.data[CONF_SCAN_INTERVAL] + hass.config_entries.async_update_entry(entry=config_entry, options=options) + + async def async_update(call=None): + """Update all data.""" + await account.update() + await account.update_obd() + + hass.services.async_register(DOMAIN, SERVICE_UPDATE_STATE, async_update) hass.services.async_register( DOMAIN, SERVICE_SET_SCAN_INTERVAL, @@ -62,6 +77,18 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b } ), ) + hass.services.async_register( + DOMAIN, + SERVICE_SET_SCAN_OBD_INTERVAL, + async_set_scan_obd_interval, + schema=vol.Schema( + { + vol.Required(CONF_SCAN_INTERVAL): vol.All( + vol.Coerce(int), vol.Range(min=180) + ) + } + ), + ) config_entry.add_update_listener(async_options_updated) await async_options_updated(hass, config_entry) @@ -83,4 +110,8 @@ async def async_options_updated(hass: HomeAssistant, config_entry: ConfigEntry) """Triggered by config entry options updates.""" account: StarlineAccount = hass.data[DOMAIN][config_entry.entry_id] scan_interval = config_entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) + scan_obd_interval = config_entry.options.get( + CONF_SCAN_OBD_INTERVAL, DEFAULT_SCAN_OBD_INTERVAL + ) account.set_update_interval(scan_interval) + account.set_update_obd_interval(scan_obd_interval) diff --git a/homeassistant/components/starline/account.py b/homeassistant/components/starline/account.py index 1cedcef8e84..7452253019b 100644 --- a/homeassistant/components/starline/account.py +++ b/homeassistant/components/starline/account.py @@ -15,6 +15,7 @@ from .const import ( DATA_SLNET_TOKEN, DATA_USER_ID, DEFAULT_SCAN_INTERVAL, + DEFAULT_SCAN_OBD_INTERVAL, DOMAIN, ) @@ -27,17 +28,19 @@ class StarlineAccount: self._hass: HomeAssistant = hass self._config_entry: ConfigEntry = config_entry self._update_interval: int = DEFAULT_SCAN_INTERVAL + self._update_obd_interval: int = DEFAULT_SCAN_OBD_INTERVAL self._unsubscribe_auto_updater: Optional[Callable] = None + self._unsubscribe_auto_obd_updater: Optional[Callable] = None self._api: StarlineApi = StarlineApi( config_entry.data[DATA_USER_ID], config_entry.data[DATA_SLNET_TOKEN] ) - def _check_slnet_token(self) -> None: + def _check_slnet_token(self, interval: int) -> None: """Check SLNet token expiration and update if needed.""" now = datetime.now().timestamp() slnet_token_expires = self._config_entry.data[DATA_EXPIRES] - if now + self._update_interval > slnet_token_expires: + if now + interval > slnet_token_expires: self._update_slnet_token() def _update_slnet_token(self) -> None: @@ -64,9 +67,14 @@ class StarlineAccount: def _update_data(self): """Update StarLine data.""" - self._check_slnet_token() + self._check_slnet_token(self._update_interval) self._api.update() + def _update_obd_data(self): + """Update StarLine OBD data.""" + self._check_slnet_token(self._update_obd_interval) + self._api.update_obd() + @property def api(self) -> StarlineApi: """Return the instance of the API.""" @@ -76,6 +84,10 @@ class StarlineAccount: """Update StarLine data.""" await self._hass.async_add_executor_job(self._update_data) + async def update_obd(self, unused=None): + """Update StarLine OBD data.""" + await self._hass.async_add_executor_job(self._update_obd_data) + def set_update_interval(self, interval: int) -> None: """Set StarLine API update interval.""" _LOGGER.debug("Setting update interval: %ds", interval) @@ -88,12 +100,27 @@ class StarlineAccount: self._hass, self.update, delta ) + def set_update_obd_interval(self, interval: int) -> None: + """Set StarLine API OBD update interval.""" + _LOGGER.debug("Setting OBD update interval: %ds", interval) + self._update_obd_interval = interval + if self._unsubscribe_auto_obd_updater is not None: + self._unsubscribe_auto_obd_updater() + + delta = timedelta(seconds=interval) + self._unsubscribe_auto_obd_updater = async_track_time_interval( + self._hass, self.update_obd, delta + ) + def unload(self): """Unload StarLine API.""" _LOGGER.debug("Unloading StarLine API.") if self._unsubscribe_auto_updater is not None: self._unsubscribe_auto_updater() self._unsubscribe_auto_updater = None + if self._unsubscribe_auto_obd_updater is not None: + self._unsubscribe_auto_obd_updater() + self._unsubscribe_auto_obd_updater = None @staticmethod def device_info(device: StarlineDevice) -> Dict[str, Any]: @@ -140,3 +167,8 @@ class StarlineAccount: "autostart": device.car_state.get("r_start"), "ignition": device.car_state.get("run"), } + + @staticmethod + def errors_attrs(device: StarlineDevice) -> Dict[str, Any]: + """Attributes for errors sensor.""" + return {"errors": device.errors.get("errors")} diff --git a/homeassistant/components/starline/const.py b/homeassistant/components/starline/const.py index 64ba8fc3d2d..89ea0873aa1 100644 --- a/homeassistant/components/starline/const.py +++ b/homeassistant/components/starline/const.py @@ -13,6 +13,8 @@ CONF_CAPTCHA_CODE = "captcha_code" CONF_SCAN_INTERVAL = "scan_interval" DEFAULT_SCAN_INTERVAL = 180 # in seconds +CONF_SCAN_OBD_INTERVAL = "scan_obd_interval" +DEFAULT_SCAN_OBD_INTERVAL = 10800 # 3 hours in seconds ERROR_AUTH_APP = "error_auth_app" ERROR_AUTH_USER = "error_auth_user" @@ -25,3 +27,4 @@ DATA_EXPIRES = "expires" SERVICE_UPDATE_STATE = "update_state" SERVICE_SET_SCAN_INTERVAL = "set_scan_interval" +SERVICE_SET_SCAN_OBD_INTERVAL = "set_scan_obd_interval" diff --git a/homeassistant/components/starline/manifest.json b/homeassistant/components/starline/manifest.json index d0cba029787..79b163ee115 100644 --- a/homeassistant/components/starline/manifest.json +++ b/homeassistant/components/starline/manifest.json @@ -3,6 +3,6 @@ "name": "StarLine", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/starline", - "requirements": ["starline==0.1.3"], + "requirements": ["starline==0.1.5"], "codeowners": ["@anonym-tsk"] } diff --git a/homeassistant/components/starline/sensor.py b/homeassistant/components/starline/sensor.py index 2de4647aa94..8aba1b54269 100644 --- a/homeassistant/components/starline/sensor.py +++ b/homeassistant/components/starline/sensor.py @@ -1,6 +1,12 @@ """Reads vehicle status from StarLine API.""" from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS, VOLT +from homeassistant.const import ( + LENGTH_KILOMETERS, + PERCENTAGE, + TEMP_CELSIUS, + VOLT, + VOLUME_LITERS, +) from homeassistant.helpers.entity import Entity from homeassistant.helpers.icon import icon_for_battery_level, icon_for_signal_level @@ -14,6 +20,9 @@ SENSOR_TYPES = { "ctemp": ["Interior Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None], "etemp": ["Engine Temperature", DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, None], "gsm_lvl": ["GSM Signal", None, PERCENTAGE, None], + "fuel": ["Fuel Volume", None, None, "mdi:fuel"], + "errors": ["OBD Errors", None, None, "mdi:alert-octagon"], + "mileage": ["Mileage", None, LENGTH_KILOMETERS, "mdi:counter"], } @@ -73,6 +82,12 @@ class StarlineSensor(StarlineEntity, Entity): return self._device.temp_engine if self._key == "gsm_lvl": return self._device.gsm_level_percent + if self._key == "fuel" and self._device.fuel: + return self._device.fuel.get("val") + if self._key == "errors" and self._device.errors: + return self._device.errors.get("val") + if self._key == "mileage" and self._device.mileage: + return self._device.mileage.get("val") return None @property @@ -80,6 +95,12 @@ class StarlineSensor(StarlineEntity, Entity): """Get the unit of measurement.""" if self._key == "balance": return self._device.balance.get("currency") or "₽" + if self._key == "fuel": + type_value = self._device.fuel.get("type") + if type_value == "percents": + return PERCENTAGE + if type_value == "litres": + return VOLUME_LITERS return self._unit @property @@ -94,4 +115,6 @@ class StarlineSensor(StarlineEntity, Entity): return self._account.balance_attrs(self._device) if self._key == "gsm_lvl": return self._account.gsm_attrs(self._device) + if self._key == "errors": + return self._account.errors_attrs(self._device) return None diff --git a/homeassistant/components/starline/services.yaml b/homeassistant/components/starline/services.yaml index bef3a16803e..4eab51b94d7 100644 --- a/homeassistant/components/starline/services.yaml +++ b/homeassistant/components/starline/services.yaml @@ -8,3 +8,10 @@ set_scan_interval: scan_interval: description: Update frequency (in seconds). example: 180 +set_scan_obd_interval: + description: > + Set OBD info update frequency. + fields: + scan_interval: + description: Update frequency (in seconds). + example: 10800 diff --git a/homeassistant/components/starline/translations/tr.json b/homeassistant/components/starline/translations/tr.json new file mode 100644 index 00000000000..9d52f589e98 --- /dev/null +++ b/homeassistant/components/starline/translations/tr.json @@ -0,0 +1,33 @@ +{ + "config": { + "error": { + "error_auth_user": "Yanl\u0131\u015f kullan\u0131c\u0131 ad\u0131 ya da parola" + }, + "step": { + "auth_app": { + "title": "Uygulama kimlik bilgileri" + }, + "auth_captcha": { + "data": { + "captcha_code": "G\u00f6r\u00fcnt\u00fcden kod" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "SMS kodu" + }, + "description": "{phone_number} telefona g\u00f6nderilen kodu girin", + "title": "\u0130ki fakt\u00f6rl\u00fc yetkilendirme" + }, + "auth_user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "StarLine hesab\u0131 e-postas\u0131 ve parolas\u0131" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/starline/translations/uk.json b/homeassistant/components/starline/translations/uk.json new file mode 100644 index 00000000000..8a263044284 --- /dev/null +++ b/homeassistant/components/starline/translations/uk.json @@ -0,0 +1,41 @@ +{ + "config": { + "error": { + "error_auth_app": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u0456\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0434\u043e\u0434\u0430\u0442\u043a\u0430 \u0430\u0431\u043e \u0441\u0435\u043a\u0440\u0435\u0442\u043d\u0438\u0439 \u043a\u043e\u0434.", + "error_auth_mfa": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043a\u043e\u0434.", + "error_auth_user": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439 \u043b\u043e\u0433\u0456\u043d \u0430\u0431\u043e \u043f\u0430\u0440\u043e\u043b\u044c." + }, + "step": { + "auth_app": { + "data": { + "app_id": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440 \u0434\u043e\u0434\u0430\u0442\u043a\u0430", + "app_secret": "\u0421\u0435\u043a\u0440\u0435\u0442\u043d\u0438\u0439 \u043a\u043e\u0434" + }, + "description": "ID \u0434\u043e\u0434\u0430\u0442\u043a\u0430 \u0456 \u0441\u0435\u043a\u0440\u0435\u0442\u043d\u0438\u0439 \u043a\u043e\u0434 [\u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 \u0440\u043e\u0437\u0440\u043e\u0431\u043d\u0438\u043a\u0430 StarLine] (https://my.starline.ru/developer)", + "title": "\u041e\u0431\u043b\u0456\u043a\u043e\u0432\u0456 \u0434\u0430\u043d\u0456 \u0434\u043e\u0434\u0430\u0442\u043a\u0430" + }, + "auth_captcha": { + "data": { + "captcha_code": "\u041a\u043e\u0434 \u0437 \u0437\u043e\u0431\u0440\u0430\u0436\u0435\u043d\u043d\u044f" + }, + "description": "{captcha_img}", + "title": "CAPTCHA" + }, + "auth_mfa": { + "data": { + "mfa_code": "\u041a\u043e\u0434 \u0437 SMS" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043a\u043e\u0434, \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u0439 \u043d\u0430 \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0443 {phone_number}", + "title": "\u0414\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f" + }, + "auth_user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438 \u0442\u0430 \u043f\u0430\u0440\u043e\u043b\u044c \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 StarLine", + "title": "\u041e\u0431\u043b\u0456\u043a\u043e\u0432\u0456 \u0434\u0430\u043d\u0456 \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/stream/__init__.py b/homeassistant/components/stream/__init__.py index 2b242389ef0..c7d1dad4835 100644 --- a/homeassistant/components/stream/__init__.py +++ b/homeassistant/components/stream/__init__.py @@ -27,8 +27,6 @@ from .hls import async_setup_hls _LOGGER = logging.getLogger(__name__) -CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) - STREAM_SERVICE_SCHEMA = vol.Schema({vol.Required(CONF_STREAM_SOURCE): cv.string}) SERVICE_RECORD_SCHEMA = STREAM_SERVICE_SCHEMA.extend( diff --git a/homeassistant/components/stream/core.py b/homeassistant/components/stream/core.py index 20931abf11e..5158ba185b1 100644 --- a/homeassistant/components/stream/core.py +++ b/homeassistant/components/stream/core.py @@ -117,9 +117,13 @@ class StreamOutput: self._cursor = segment.sequence return segment - @callback def put(self, segment: Segment) -> None: """Store output.""" + self._stream.hass.loop.call_soon_threadsafe(self._async_put, segment) + + @callback + def _async_put(self, segment: Segment) -> None: + """Store output from event loop.""" # Start idle timeout when we start receiving data if self._unsub is None: self._unsub = async_call_later( diff --git a/homeassistant/components/stream/hls.py b/homeassistant/components/stream/hls.py index 92801c4807f..2b305442b80 100644 --- a/homeassistant/components/stream/hls.py +++ b/homeassistant/components/stream/hls.py @@ -52,7 +52,8 @@ class HlsMasterPlaylistView(StreamView): stream.start() # Wait for a segment to be ready if not track.segments: - await track.recv() + if not await track.recv(): + return web.HTTPNotFound() headers = {"Content-Type": FORMAT_CONTENT_TYPE["hls"]} return web.Response(body=self.render(track).encode("utf-8"), headers=headers) @@ -105,7 +106,8 @@ class HlsPlaylistView(StreamView): stream.start() # Wait for a segment to be ready if not track.segments: - await track.recv() + if not await track.recv(): + return web.HTTPNotFound() headers = {"Content-Type": FORMAT_CONTENT_TYPE["hls"]} return web.Response(body=self.render(track).encode("utf-8"), headers=headers) diff --git a/homeassistant/components/stream/recorder.py b/homeassistant/components/stream/recorder.py index 420e7c654c5..cf923de85c2 100644 --- a/homeassistant/components/stream/recorder.py +++ b/homeassistant/components/stream/recorder.py @@ -1,4 +1,5 @@ """Provide functionality to record stream.""" +import logging import os import threading from typing import List @@ -9,6 +10,8 @@ from homeassistant.core import callback from .core import PROVIDERS, Segment, StreamOutput +_LOGGER = logging.getLogger(__name__) + @callback def async_setup_recorder(hass): @@ -109,6 +112,7 @@ class RecorderOutput(StreamOutput): def cleanup(self): """Write recording and clean up.""" + _LOGGER.debug("Starting recorder worker thread") thread = threading.Thread( name="recorder_save_worker", target=recorder_save_worker, diff --git a/homeassistant/components/stream/worker.py b/homeassistant/components/stream/worker.py index 68cbbc79726..cccbfd1b48b 100644 --- a/homeassistant/components/stream/worker.py +++ b/homeassistant/components/stream/worker.py @@ -224,7 +224,7 @@ def _stream_worker_internal(hass, stream, quit_event): if not stream.keepalive: # End of stream, clear listeners and stop thread for fmt in stream.outputs: - hass.loop.call_soon_threadsafe(stream.outputs[fmt].put, None) + stream.outputs[fmt].put(None) if not peek_first_pts(): container.close() @@ -275,8 +275,7 @@ def _stream_worker_internal(hass, stream, quit_event): for fmt, (buffer, _) in outputs.items(): buffer.output.close() if stream.outputs.get(fmt): - hass.loop.call_soon_threadsafe( - stream.outputs[fmt].put, + stream.outputs[fmt].put( Segment( sequence, buffer.segment, diff --git a/homeassistant/components/sun/translations/pl.json b/homeassistant/components/sun/translations/pl.json index fb90b9bd232..1f00babd1fd 100644 --- a/homeassistant/components/sun/translations/pl.json +++ b/homeassistant/components/sun/translations/pl.json @@ -1,7 +1,7 @@ { "state": { "_": { - "above_horizon": "powy\u017cej horyzontu", + "above_horizon": "nad horyzontem", "below_horizon": "poni\u017cej horyzontu" } }, diff --git a/homeassistant/components/surepetcare/__init__.py b/homeassistant/components/surepetcare/__init__.py index 9aac8f11941..8ba6809ee05 100644 --- a/homeassistant/components/surepetcare/__init__.py +++ b/homeassistant/components/surepetcare/__init__.py @@ -3,10 +3,12 @@ import logging from typing import Any, Dict, List from surepy import ( + MESTART_RESOURCE, + SureLockStateID, SurePetcare, SurePetcareAuthenticationError, SurePetcareError, - SureProductID, + SurepyProduct, ) import voluptuous as vol @@ -23,6 +25,8 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from .const import ( + ATTR_FLAP_ID, + ATTR_LOCK_STATE, CONF_FEEDERS, CONF_FLAPS, CONF_PARENT, @@ -31,6 +35,7 @@ from .const import ( DATA_SURE_PETCARE, DEFAULT_SCAN_INTERVAL, DOMAIN, + SERVICE_SET_LOCK_STATE, SPC, SURE_API_TIMEOUT, TOPIC_UPDATE, @@ -81,7 +86,7 @@ async def async_setup(hass, config) -> bool: async_get_clientsession(hass), api_timeout=SURE_API_TIMEOUT, ) - await surepy.get_data() + except SurePetcareAuthenticationError: _LOGGER.error("Unable to connect to surepetcare.io: Wrong credentials!") return False @@ -91,14 +96,14 @@ async def async_setup(hass, config) -> bool: # add feeders things = [ - {CONF_ID: feeder, CONF_TYPE: SureProductID.FEEDER} + {CONF_ID: feeder, CONF_TYPE: SurepyProduct.FEEDER} for feeder in conf[CONF_FEEDERS] ] # add flaps (don't differentiate between CAT and PET for now) things.extend( [ - {CONF_ID: flap, CONF_TYPE: SureProductID.PET_FLAP} + {CONF_ID: flap, CONF_TYPE: SurepyProduct.PET_FLAP} for flap in conf[CONF_FLAPS] ] ) @@ -109,20 +114,20 @@ async def async_setup(hass, config) -> bool: device_data = await surepy.device(device[CONF_ID]) if ( CONF_PARENT in device_data - and device_data[CONF_PARENT][CONF_PRODUCT_ID] == SureProductID.HUB + and device_data[CONF_PARENT][CONF_PRODUCT_ID] == SurepyProduct.HUB and device_data[CONF_PARENT][CONF_ID] not in hub_ids ): things.append( { CONF_ID: device_data[CONF_PARENT][CONF_ID], - CONF_TYPE: SureProductID.HUB, + CONF_TYPE: SurepyProduct.HUB, } ) hub_ids.add(device_data[CONF_PARENT][CONF_ID]) # add pets things.extend( - [{CONF_ID: pet, CONF_TYPE: SureProductID.PET} for pet in conf[CONF_PETS]] + [{CONF_ID: pet, CONF_TYPE: SurepyProduct.PET} for pet in conf[CONF_PETS]] ) _LOGGER.debug("Devices and Pets to setup: %s", things) @@ -142,6 +147,38 @@ async def async_setup(hass, config) -> bool: hass.helpers.discovery.async_load_platform("sensor", DOMAIN, {}, config) ) + async def handle_set_lock_state(call): + """Call when setting the lock state.""" + await spc.set_lock_state(call.data[ATTR_FLAP_ID], call.data[ATTR_LOCK_STATE]) + await spc.async_update() + + lock_state_service_schema = vol.Schema( + { + vol.Required(ATTR_FLAP_ID): vol.All( + cv.positive_int, vol.In(conf[CONF_FLAPS]) + ), + vol.Required(ATTR_LOCK_STATE): vol.All( + cv.string, + vol.Lower, + vol.In( + [ + SureLockStateID.UNLOCKED.name.lower(), + SureLockStateID.LOCKED_IN.name.lower(), + SureLockStateID.LOCKED_OUT.name.lower(), + SureLockStateID.LOCKED_ALL.name.lower(), + ] + ), + ), + } + ) + + hass.services.async_register( + DOMAIN, + SERVICE_SET_LOCK_STATE, + handle_set_lock_state, + schema=lock_state_service_schema, + ) + return True @@ -158,8 +195,11 @@ class SurePetcareAPI: async def async_update(self, arg: Any = None) -> None: """Refresh Sure Petcare data.""" - await self.surepy.get_data() - + # Fetch all data from SurePet API, refreshing the surepy cache + # TODO: get surepy upstream to add a method to clear the cache explicitly pylint: disable=fixme + await self.surepy._get_resource( # pylint: disable=protected-access + resource=MESTART_RESOURCE + ) for thing in self.ids: sure_id = thing[CONF_ID] sure_type = thing[CONF_TYPE] @@ -168,16 +208,27 @@ class SurePetcareAPI: type_state = self.states.setdefault(sure_type, {}) if sure_type in [ - SureProductID.CAT_FLAP, - SureProductID.PET_FLAP, - SureProductID.FEEDER, - SureProductID.HUB, + SurepyProduct.CAT_FLAP, + SurepyProduct.PET_FLAP, + SurepyProduct.FEEDER, + SurepyProduct.HUB, ]: type_state[sure_id] = await self.surepy.device(sure_id) - elif sure_type == SureProductID.PET: + elif sure_type == SurepyProduct.PET: type_state[sure_id] = await self.surepy.pet(sure_id) except SurePetcareError as error: _LOGGER.error("Unable to retrieve data from surepetcare.io: %s", error) async_dispatcher_send(self.hass, TOPIC_UPDATE) + + async def set_lock_state(self, flap_id: int, state: str) -> None: + """Update the lock state of a flap.""" + if state == SureLockStateID.UNLOCKED.name.lower(): + await self.surepy.unlock(flap_id) + elif state == SureLockStateID.LOCKED_IN.name.lower(): + await self.surepy.lock_in(flap_id) + elif state == SureLockStateID.LOCKED_OUT.name.lower(): + await self.surepy.lock_out(flap_id) + elif state == SureLockStateID.LOCKED_ALL.name.lower(): + await self.surepy.lock(flap_id) diff --git a/homeassistant/components/surepetcare/binary_sensor.py b/homeassistant/components/surepetcare/binary_sensor.py index 0cb96731058..2a624b580ac 100644 --- a/homeassistant/components/surepetcare/binary_sensor.py +++ b/homeassistant/components/surepetcare/binary_sensor.py @@ -3,7 +3,7 @@ from datetime import datetime import logging from typing import Any, Dict, Optional -from surepy import SureLocationID, SureProductID +from surepy import SureLocationID, SurepyProduct from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, @@ -37,15 +37,15 @@ async def async_setup_platform( # connectivity if sure_type in [ - SureProductID.CAT_FLAP, - SureProductID.PET_FLAP, - SureProductID.FEEDER, + SurepyProduct.CAT_FLAP, + SurepyProduct.PET_FLAP, + SurepyProduct.FEEDER, ]: entities.append(DeviceConnectivity(sure_id, sure_type, spc)) - if sure_type == SureProductID.PET: + if sure_type == SurepyProduct.PET: entity = Pet(sure_id, spc) - elif sure_type == SureProductID.HUB: + elif sure_type == SurepyProduct.HUB: entity = Hub(sure_id, spc) else: continue @@ -63,7 +63,7 @@ class SurePetcareBinarySensor(BinarySensorEntity): _id: int, spc: SurePetcareAPI, device_class: str, - sure_type: SureProductID, + sure_type: SurepyProduct, ): """Initialize a Sure Petcare binary sensor.""" self._id = _id @@ -138,7 +138,7 @@ class Hub(SurePetcareBinarySensor): def __init__(self, _id: int, spc: SurePetcareAPI) -> None: """Initialize a Sure Petcare Hub.""" - super().__init__(_id, spc, DEVICE_CLASS_CONNECTIVITY, SureProductID.HUB) + super().__init__(_id, spc, DEVICE_CLASS_CONNECTIVITY, SurepyProduct.HUB) @property def available(self) -> bool: @@ -168,7 +168,7 @@ class Pet(SurePetcareBinarySensor): def __init__(self, _id: int, spc: SurePetcareAPI) -> None: """Initialize a Sure Petcare Pet.""" - super().__init__(_id, spc, DEVICE_CLASS_PRESENCE, SureProductID.PET) + super().__init__(_id, spc, DEVICE_CLASS_PRESENCE, SurepyProduct.PET) @property def is_on(self) -> bool: @@ -205,7 +205,7 @@ class DeviceConnectivity(SurePetcareBinarySensor): def __init__( self, _id: int, - sure_type: SureProductID, + sure_type: SurepyProduct, spc: SurePetcareAPI, ) -> None: """Initialize a Sure Petcare Device.""" diff --git a/homeassistant/components/surepetcare/const.py b/homeassistant/components/surepetcare/const.py index 165e0bfd98a..86215c12ade 100644 --- a/homeassistant/components/surepetcare/const.py +++ b/homeassistant/components/surepetcare/const.py @@ -31,3 +31,8 @@ BATTERY_ICON = "mdi:battery" SURE_BATT_VOLTAGE_FULL = 1.6 # voltage SURE_BATT_VOLTAGE_LOW = 1.25 # voltage SURE_BATT_VOLTAGE_DIFF = SURE_BATT_VOLTAGE_FULL - SURE_BATT_VOLTAGE_LOW + +# lock state service +SERVICE_SET_LOCK_STATE = "set_lock_state" +ATTR_FLAP_ID = "flap_id" +ATTR_LOCK_STATE = "lock_state" diff --git a/homeassistant/components/surepetcare/manifest.json b/homeassistant/components/surepetcare/manifest.json index 2fbe4fe245f..99b52a68c8d 100644 --- a/homeassistant/components/surepetcare/manifest.json +++ b/homeassistant/components/surepetcare/manifest.json @@ -3,5 +3,5 @@ "name": "Sure Petcare", "documentation": "https://www.home-assistant.io/integrations/surepetcare", "codeowners": ["@benleb"], - "requirements": ["surepy==0.2.6"] + "requirements": ["surepy==0.4.0"] } diff --git a/homeassistant/components/surepetcare/sensor.py b/homeassistant/components/surepetcare/sensor.py index d3fcb41dbf4..e2d3d070867 100644 --- a/homeassistant/components/surepetcare/sensor.py +++ b/homeassistant/components/surepetcare/sensor.py @@ -2,7 +2,7 @@ import logging from typing import Any, Dict, Optional -from surepy import SureLockStateID, SureProductID +from surepy import SureLockStateID, SurepyProduct from homeassistant.const import ( ATTR_VOLTAGE, @@ -40,13 +40,13 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= sure_type = entity[CONF_TYPE] if sure_type in [ - SureProductID.CAT_FLAP, - SureProductID.PET_FLAP, - SureProductID.FEEDER, + SurepyProduct.CAT_FLAP, + SurepyProduct.PET_FLAP, + SurepyProduct.FEEDER, ]: entities.append(SureBattery(entity[CONF_ID], sure_type, spc)) - if sure_type in [SureProductID.CAT_FLAP, SureProductID.PET_FLAP]: + if sure_type in [SurepyProduct.CAT_FLAP, SurepyProduct.PET_FLAP]: entities.append(Flap(entity[CONF_ID], sure_type, spc)) async_add_entities(entities, True) @@ -55,7 +55,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class SurePetcareSensor(Entity): """A binary sensor implementation for Sure Petcare Entities.""" - def __init__(self, _id: int, sure_type: SureProductID, spc: SurePetcareAPI): + def __init__(self, _id: int, sure_type: SurepyProduct, spc: SurePetcareAPI): """Initialize a Sure Petcare sensor.""" self._id = _id diff --git a/homeassistant/components/surepetcare/services.yaml b/homeassistant/components/surepetcare/services.yaml new file mode 100644 index 00000000000..145256efe86 --- /dev/null +++ b/homeassistant/components/surepetcare/services.yaml @@ -0,0 +1,9 @@ +set_lock_state: + description: Sets lock state + fields: + flap_id: + description: Flap ID to lock/unlock + example: "123456" + lock_state: + description: New lock state - unlocked, locked_in, locked_out or locked_all + example: "unlocked" diff --git a/homeassistant/components/switch/significant_change.py b/homeassistant/components/switch/significant_change.py new file mode 100644 index 00000000000..f4dcddc3f34 --- /dev/null +++ b/homeassistant/components/switch/significant_change.py @@ -0,0 +1,17 @@ +"""Helper to test significant Switch state changes.""" +from typing import Any, Optional + +from homeassistant.core import HomeAssistant, callback + + +@callback +def async_check_significant_change( + hass: HomeAssistant, + old_state: str, + old_attrs: dict, + new_state: str, + new_attrs: dict, + **kwargs: Any, +) -> Optional[bool]: + """Test if state significantly changed.""" + return old_state != new_state diff --git a/homeassistant/components/switch/translations/uk.json b/homeassistant/components/switch/translations/uk.json index bee9eb957d5..26b85b3a873 100644 --- a/homeassistant/components/switch/translations/uk.json +++ b/homeassistant/components/switch/translations/uk.json @@ -1,5 +1,14 @@ { "device_automation": { + "action_type": { + "toggle": "{entity_name}: \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u0438", + "turn_off": "{entity_name}: \u0432\u0438\u043c\u043a\u043d\u0443\u0442\u0438", + "turn_on": "{entity_name}: \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438" + }, + "condition_type": { + "is_off": "{entity_name} \u0443 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456", + "is_on": "{entity_name} \u0443 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e\u043c\u0443 \u0441\u0442\u0430\u043d\u0456" + }, "trigger_type": { "turned_off": "{entity_name} \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043e", "turned_on": "{entity_name} \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e" diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index 23b69eefb74..244ed708cc7 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -4,35 +4,22 @@ from datetime import datetime, timedelta import logging from typing import Dict, Optional -from aioswitcher.api import SwitcherV2Api from aioswitcher.bridge import SwitcherV2Bridge -from aioswitcher.consts import COMMAND_ON import voluptuous as vol -from homeassistant.auth.permissions.const import POLICY_EDIT from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_STOP -from homeassistant.core import callback, split_entity_id -from homeassistant.exceptions import Unauthorized, UnknownUser +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.discovery import async_listen_platform, async_load_platform +from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval -from homeassistant.helpers.typing import ( - ContextType, - DiscoveryInfoType, - EventType, - HomeAssistantType, - ServiceCallType, -) -from homeassistant.loader import bind_hass +from homeassistant.helpers.typing import EventType, HomeAssistantType _LOGGER = logging.getLogger(__name__) DOMAIN = "switcher_kis" -CONF_AUTO_OFF = "auto_off" -CONF_TIMER_MINUTES = "timer_minutes" CONF_DEVICE_ID = "device_id" CONF_DEVICE_PASSWORD = "device_password" CONF_PHONE_ID = "phone_id" @@ -58,39 +45,6 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -SERVICE_SET_AUTO_OFF_NAME = "set_auto_off" -SERVICE_SET_AUTO_OFF_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_id, - vol.Required(CONF_AUTO_OFF): cv.time_period_str, - } -) - -SERVICE_TURN_ON_WITH_TIMER_NAME = "turn_on_with_timer" -SERVICE_TURN_ON_WITH_TIMER_SCHEMA = vol.Schema( - { - vol.Required(ATTR_ENTITY_ID): cv.entity_id, - vol.Required(CONF_TIMER_MINUTES): vol.All( - cv.positive_int, vol.Range(min=1, max=90) - ), - } -) - - -@bind_hass -async def _validate_edit_permission( - hass: HomeAssistantType, context: ContextType, entity_id: str -) -> None: - """Use for validating user control permissions.""" - splited = split_entity_id(entity_id) - if splited[0] != SWITCH_DOMAIN or not splited[1].startswith(DOMAIN): - raise Unauthorized(context=context, entity_id=entity_id, permission=POLICY_EDIT) - user = await hass.auth.async_get_user(context.user_id) - if user is None: - raise UnknownUser(context=context, entity_id=entity_id, permission=POLICY_EDIT) - if not user.permissions.check_entity(entity_id, POLICY_EDIT): - raise Unauthorized(context=context, entity_id=entity_id, permission=POLICY_EDIT) - async def async_setup(hass: HomeAssistantType, config: Dict) -> bool: """Set up the switcher component.""" @@ -117,53 +71,6 @@ async def async_setup(hass: HomeAssistantType, config: Dict) -> bool: return False hass.data[DOMAIN] = {DATA_DEVICE: device_data} - async def async_switch_platform_discovered( - platform: str, discovery_info: DiscoveryInfoType - ) -> None: - """Use for registering services after switch platform is discovered.""" - if platform != DOMAIN: - return - - async def async_set_auto_off_service(service: ServiceCallType) -> None: - """Use for handling setting device auto-off service calls.""" - - await _validate_edit_permission( - hass, service.context, service.data[ATTR_ENTITY_ID] - ) - - async with SwitcherV2Api( - hass.loop, device_data.ip_addr, phone_id, device_id, device_password - ) as swapi: - await swapi.set_auto_shutdown(service.data[CONF_AUTO_OFF]) - - async def async_turn_on_with_timer_service(service: ServiceCallType) -> None: - """Use for handling turning device on with a timer service calls.""" - - await _validate_edit_permission( - hass, service.context, service.data[ATTR_ENTITY_ID] - ) - - async with SwitcherV2Api( - hass.loop, device_data.ip_addr, phone_id, device_id, device_password - ) as swapi: - await swapi.control_device(COMMAND_ON, service.data[CONF_TIMER_MINUTES]) - - hass.services.async_register( - DOMAIN, - SERVICE_SET_AUTO_OFF_NAME, - async_set_auto_off_service, - schema=SERVICE_SET_AUTO_OFF_SCHEMA, - ) - - hass.services.async_register( - DOMAIN, - SERVICE_TURN_ON_WITH_TIMER_NAME, - async_turn_on_with_timer_service, - schema=SERVICE_TURN_ON_WITH_TIMER_SCHEMA, - ) - - async_listen_platform(hass, SWITCH_DOMAIN, async_switch_platform_discovered) - hass.async_create_task(async_load_platform(hass, SWITCH_DOMAIN, DOMAIN, {}, config)) @callback diff --git a/homeassistant/components/switcher_kis/services.yaml b/homeassistant/components/switcher_kis/services.yaml index 07e0cfe1198..be812f8d7e1 100644 --- a/homeassistant/components/switcher_kis/services.yaml +++ b/homeassistant/components/switcher_kis/services.yaml @@ -15,5 +15,5 @@ turn_on_with_timer: description: "Name of the entity id associated with the integration, used for permission validation." example: "switch.switcher_kis_boiler" timer_minutes: - description: 'Minutes to turn on (valid range from 1 to 90)' + description: 'Minutes to turn on (valid range from 1 to 150)' example: '30' diff --git a/homeassistant/components/switcher_kis/switch.py b/homeassistant/components/switcher_kis/switch.py index af922c3ab8b..5e75a0e6090 100644 --- a/homeassistant/components/switcher_kis/switch.py +++ b/homeassistant/components/switcher_kis/switch.py @@ -1,7 +1,8 @@ """Home Assistant Switcher Component Switch platform.""" -from typing import TYPE_CHECKING, Callable, Dict +from typing import Callable, Dict from aioswitcher.api import SwitcherV2Api +from aioswitcher.api.messages import SwitcherV2ControlResponseMSG from aioswitcher.consts import ( COMMAND_OFF, COMMAND_ON, @@ -9,10 +10,13 @@ from aioswitcher.consts import ( STATE_ON as SWITCHER_STATE_ON, WAITING_TEXT, ) +from aioswitcher.devices import SwitcherV2Device +import voluptuous as vol from homeassistant.components.switch import ATTR_CURRENT_POWER_W, SwitchEntity +from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.helpers.typing import HomeAssistantType, ServiceCallType from . import ( ATTR_AUTO_OFF_SET, @@ -23,11 +27,8 @@ from . import ( SIGNAL_SWITCHER_DEVICE_UPDATE, ) -# pylint: disable=ungrouped-imports -if TYPE_CHECKING: - from aioswitcher.api.messages import SwitcherV2ControlResponseMSG - from aioswitcher.devices import SwitcherV2Device - +CONF_AUTO_OFF = "auto_off" +CONF_TIMER_MINUTES = "timer_minutes" DEVICE_PROPERTIES_TO_HA_ATTRIBUTES = { "power_consumption": ATTR_CURRENT_POWER_W, @@ -36,6 +37,18 @@ DEVICE_PROPERTIES_TO_HA_ATTRIBUTES = { "auto_off_set": ATTR_AUTO_OFF_SET, } +SERVICE_SET_AUTO_OFF_NAME = "set_auto_off" +SERVICE_SET_AUTO_OFF_SCHEMA = { + vol.Required(CONF_AUTO_OFF): cv.time_period_str, +} + +SERVICE_TURN_ON_WITH_TIMER_NAME = "turn_on_with_timer" +SERVICE_TURN_ON_WITH_TIMER_SCHEMA = { + vol.Required(CONF_TIMER_MINUTES): vol.All( + cv.positive_int, vol.Range(min=1, max=150) + ), +} + async def async_setup_platform( hass: HomeAssistantType, @@ -46,13 +59,57 @@ async def async_setup_platform( """Set up the switcher platform for the switch component.""" if discovery_info is None: return + + async def async_set_auto_off_service(entity, service_call: ServiceCallType) -> None: + """Use for handling setting device auto-off service calls.""" + + async with SwitcherV2Api( + hass.loop, + device_data.ip_addr, + device_data.phone_id, + device_data.device_id, + device_data.device_password, + ) as swapi: + await swapi.set_auto_shutdown(service_call.data[CONF_AUTO_OFF]) + + async def async_turn_on_with_timer_service( + entity, service_call: ServiceCallType + ) -> None: + """Use for handling turning device on with a timer service calls.""" + + async with SwitcherV2Api( + hass.loop, + device_data.ip_addr, + device_data.phone_id, + device_data.device_id, + device_data.device_password, + ) as swapi: + await swapi.control_device( + COMMAND_ON, service_call.data[CONF_TIMER_MINUTES] + ) + + device_data = hass.data[DOMAIN][DATA_DEVICE] async_add_entities([SwitcherControl(hass.data[DOMAIN][DATA_DEVICE])]) + platform = entity_platform.current_platform.get() + + platform.async_register_entity_service( + SERVICE_SET_AUTO_OFF_NAME, + SERVICE_SET_AUTO_OFF_SCHEMA, + async_set_auto_off_service, + ) + + platform.async_register_entity_service( + SERVICE_TURN_ON_WITH_TIMER_NAME, + SERVICE_TURN_ON_WITH_TIMER_SCHEMA, + async_turn_on_with_timer_service, + ) + class SwitcherControl(SwitchEntity): """Home Assistant switch entity.""" - def __init__(self, device_data: "SwitcherV2Device") -> None: + def __init__(self, device_data: SwitcherV2Device) -> None: """Initialize the entity.""" self._self_initiated = False self._device_data = device_data @@ -111,7 +168,7 @@ class SwitcherControl(SwitchEntity): ) ) - async def async_update_data(self, device_data: "SwitcherV2Device") -> None: + async def async_update_data(self, device_data: SwitcherV2Device) -> None: """Update the entity data.""" if device_data: if self._self_initiated: @@ -132,7 +189,7 @@ class SwitcherControl(SwitchEntity): async def _control_device(self, send_on: bool) -> None: """Turn the entity on or off.""" - response: "SwitcherV2ControlResponseMSG" = None + response: SwitcherV2ControlResponseMSG = None async with SwitcherV2Api( self.hass.loop, self._device_data.ip_addr, diff --git a/homeassistant/components/syncthru/translations/tr.json b/homeassistant/components/syncthru/translations/tr.json new file mode 100644 index 00000000000..942457958f8 --- /dev/null +++ b/homeassistant/components/syncthru/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "confirm": { + "data": { + "url": "Web aray\u00fcz\u00fc URL'si" + } + }, + "user": { + "data": { + "name": "Ad", + "url": "Web aray\u00fcz\u00fc URL'si" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/syncthru/translations/uk.json b/homeassistant/components/syncthru/translations/uk.json new file mode 100644 index 00000000000..74cccc7ef5a --- /dev/null +++ b/homeassistant/components/syncthru/translations/uk.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "invalid_url": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0430 URL-\u0430\u0434\u0440\u0435\u0441\u0430.", + "syncthru_not_supported": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454 SyncThru.", + "unknown_state": "\u0421\u0442\u0430\u043d \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430 \u043d\u0435\u0432\u0456\u0434\u043e\u043c\u0438\u0439, \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 URL-\u0430\u0434\u0440\u0435\u0441\u0443 \u0442\u0430 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u043c\u0435\u0440\u0435\u0436\u0456." + }, + "flow_title": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440 Samsung SyncThru: {name}", + "step": { + "confirm": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430", + "url": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0432\u0435\u0431-\u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0443" + } + }, + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430", + "url": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0432\u0435\u0431-\u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0443" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/de.json b/homeassistant/components/synology_dsm/translations/de.json index 303321ea94c..f0d274c3bfe 100644 --- a/homeassistant/components/synology_dsm/translations/de.json +++ b/homeassistant/components/synology_dsm/translations/de.json @@ -1,13 +1,14 @@ { "config": { "abort": { - "already_configured": "Host bereits konfiguriert" + "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "missing_data": "Fehlende Daten: Bitte versuchen Sie es sp\u00e4ter noch einmal oder eine andere Konfiguration", "otp_failed": "Die zweistufige Authentifizierung ist fehlgeschlagen. Versuchen Sie es erneut mit einem neuen Code", - "unknown": "Unbekannter Fehler: Bitte \u00fcberpr\u00fcfen Sie die Protokolle, um weitere Details zu erhalten" + "unknown": "Unerwarteter Fehler" }, "flow_title": "Synology DSM {name} ({host})", "step": { @@ -21,7 +22,7 @@ "data": { "password": "Passwort", "port": "Port", - "ssl": "Verwenden Sie SSL/TLS, um eine Verbindung zu Ihrem NAS herzustellen", + "ssl": "Verwendet ein SSL-Zertifikat", "username": "Benutzername", "verify_ssl": "SSL Zertifikat verifizieren" }, @@ -33,7 +34,7 @@ "host": "Host", "password": "Passwort", "port": "Port", - "ssl": "Verwenden Sie SSL/TLS, um eine Verbindung zu Ihrem NAS herzustellen", + "ssl": "Verwendet ein SSL-Zertifikat", "username": "Benutzername", "verify_ssl": "SSL Zertifikat verifizieren" }, diff --git a/homeassistant/components/synology_dsm/translations/tr.json b/homeassistant/components/synology_dsm/translations/tr.json index a7598bb3438..681d85d2ef5 100644 --- a/homeassistant/components/synology_dsm/translations/tr.json +++ b/homeassistant/components/synology_dsm/translations/tr.json @@ -1,15 +1,31 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, "step": { "link": { "data": { + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131", "verify_ssl": "SSL sertifikalar\u0131n\u0131 do\u011frula" } }, "user": { "data": { + "host": "Ana Bilgisayar", + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131", "verify_ssl": "SSL sertifikalar\u0131n\u0131 do\u011frula" - } + }, + "title": "Synology DSM" } } } diff --git a/homeassistant/components/synology_dsm/translations/uk.json b/homeassistant/components/synology_dsm/translations/uk.json new file mode 100644 index 00000000000..4d80350989f --- /dev/null +++ b/homeassistant/components/synology_dsm/translations/uk.json @@ -0,0 +1,55 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "missing_data": "\u0412\u0456\u0434\u0441\u0443\u0442\u043d\u0456 \u0434\u0430\u043d\u0456: \u043f\u043e\u0432\u0442\u043e\u0440\u0456\u0442\u044c \u0441\u043f\u0440\u043e\u0431\u0443 \u043f\u0456\u0437\u043d\u0456\u0448\u0435 \u0430\u0431\u043e \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0456\u043d\u0448\u0443 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e.", + "otp_failed": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0434\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u043e\u0457 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0449\u0435 \u0440\u0430\u0437 \u0437 \u043d\u043e\u0432\u0438\u043c \u043f\u0430\u0440\u043e\u043b\u0435\u043c.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "flow_title": "Synology DSM {name} ({host})", + "step": { + "2sa": { + "data": { + "otp_code": "\u041a\u043e\u0434" + }, + "title": "Synology DSM: \u0434\u0432\u043e\u0444\u0430\u043a\u0442\u043e\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f" + }, + "link": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", + "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL" + }, + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 {name} ({host})?", + "title": "Synology DSM" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", + "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL" + }, + "title": "Synology DSM" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043c\u0456\u0436 \u0441\u043a\u0430\u043d\u0443\u0432\u0430\u043d\u043d\u044f\u043c\u0438 (\u0445\u0432.)", + "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/system_health/translations/uk.json b/homeassistant/components/system_health/translations/uk.json index 267fcb83a61..61f30782f04 100644 --- a/homeassistant/components/system_health/translations/uk.json +++ b/homeassistant/components/system_health/translations/uk.json @@ -1,3 +1,3 @@ { - "title": "\u0411\u0435\u0437\u043f\u0435\u043a\u0430 \u0441\u0438\u0441\u0442\u0435\u043c\u0438" + "title": "\u0421\u0442\u0430\u043d \u0441\u0438\u0441\u0442\u0435\u043c\u0438" } \ No newline at end of file diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index bb255ba8bf3..9c868724d9b 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -1,5 +1,4 @@ """Support for system log.""" -import asyncio from collections import OrderedDict, deque import logging import queue @@ -165,8 +164,6 @@ class LogErrorQueueHandler(logging.handlers.QueueHandler): """Emit a log record.""" try: self.enqueue(record) - except asyncio.CancelledError: - raise except Exception: # pylint: disable=broad-except self.handleError(record) diff --git a/homeassistant/components/systemmonitor/sensor.py b/homeassistant/components/systemmonitor/sensor.py index 00f193f8663..ce856c04c64 100644 --- a/homeassistant/components/systemmonitor/sensor.py +++ b/homeassistant/components/systemmonitor/sensor.py @@ -316,9 +316,11 @@ class SystemMonitorSensor(Entity): else: self._state = None elif self.type == "last_boot": - self._state = dt_util.as_local( - dt_util.utc_from_timestamp(psutil.boot_time()) - ).isoformat() + # Only update on initial setup + if self._state is None: + self._state = dt_util.as_local( + dt_util.utc_from_timestamp(psutil.boot_time()) + ).isoformat() elif self.type == "load_1m": self._state = round(os.getloadavg()[0], 2) elif self.type == "load_5m": diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 228ac48bcb2..e88fb4c60b8 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -6,10 +6,9 @@ import logging from PyTado.interface import Tado from requests import RequestException import requests.exceptions -import voluptuous as vol from homeassistant.components.climate.const import PRESET_AWAY, PRESET_HOME -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady @@ -22,7 +21,9 @@ from .const import ( CONF_FALLBACK, DATA, DOMAIN, + INSIDE_TEMPERATURE_MEASUREMENT, SIGNAL_TADO_UPDATE_RECEIVED, + TEMP_OFFSET, UPDATE_LISTENER, UPDATE_TRACK, ) @@ -35,21 +36,7 @@ TADO_COMPONENTS = ["binary_sensor", "sensor", "climate", "water_heater"] MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=10) SCAN_INTERVAL = timedelta(seconds=15) -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.All( - cv.ensure_list, - [ - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_FALLBACK, default=True): cv.boolean, - } - ], - ) - }, - extra=vol.ALLOW_EXTRA, -) +CONFIG_SCHEMA = cv.deprecated(DOMAIN) async def async_setup(hass: HomeAssistant, config: dict): @@ -57,18 +44,6 @@ async def async_setup(hass: HomeAssistant, config: dict): hass.data.setdefault(DOMAIN, {}) - if DOMAIN not in config: - return True - - for conf in config[DOMAIN]: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data=conf, - ) - ) - return True @@ -173,6 +148,7 @@ class TadoConnector: self.zones = None self.devices = None self.data = { + "device": {}, "zone": {}, } @@ -193,15 +169,23 @@ class TadoConnector: @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update the registered zones.""" + for device in self.devices: + self.update_sensor("device", device["shortSerialNo"]) for zone in self.zones: self.update_sensor("zone", zone["id"]) - self.devices = self.tado.getDevices() def update_sensor(self, sensor_type, sensor): """Update the internal data from Tado.""" _LOGGER.debug("Updating %s %s", sensor_type, sensor) try: - if sensor_type == "zone": + if sensor_type == "device": + data = self.tado.getDeviceInfo(sensor) + if ( + INSIDE_TEMPERATURE_MEASUREMENT + in data["characteristics"]["capabilities"] + ): + data[TEMP_OFFSET] = self.tado.getDeviceInfo(sensor, TEMP_OFFSET) + elif sensor_type == "zone": data = self.tado.getZoneState(sensor) else: _LOGGER.debug("Unknown sensor: %s", sensor_type) @@ -299,3 +283,10 @@ class TadoConnector: _LOGGER.error("Could not set zone overlay: %s", exc) self.update_sensor("zone", zone_id) + + def set_temperature_offset(self, device_id, offset): + """Set temperature offset of device.""" + try: + self.tado.setTempOffset(device_id, offset) + except RequestException as exc: + _LOGGER.error("Could not set temperature offset: %s", exc) diff --git a/homeassistant/components/tado/binary_sensor.py b/homeassistant/components/tado/binary_sensor.py index 279633b07b1..1acefdb4c16 100644 --- a/homeassistant/components/tado/binary_sensor.py +++ b/homeassistant/components/tado/binary_sensor.py @@ -4,14 +4,25 @@ import logging from homeassistant.components.binary_sensor import ( DEVICE_CLASS_BATTERY, DEVICE_CLASS_CONNECTIVITY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_WINDOW, BinarySensorEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import DATA, DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED, TYPE_BATTERY, TYPE_POWER -from .entity import TadoDeviceEntity +from .const import ( + DATA, + DOMAIN, + SIGNAL_TADO_UPDATE_RECEIVED, + TYPE_AIR_CONDITIONING, + TYPE_BATTERY, + TYPE_HEATING, + TYPE_HOT_WATER, + TYPE_POWER, +) +from .entity import TadoDeviceEntity, TadoZoneEntity _LOGGER = logging.getLogger(__name__) @@ -25,6 +36,23 @@ DEVICE_SENSORS = { ], } +ZONE_SENSORS = { + TYPE_HEATING: [ + "power", + "link", + "overlay", + "early start", + "open window", + ], + TYPE_AIR_CONDITIONING: [ + "power", + "link", + "overlay", + "open window", + ], + TYPE_HOT_WATER: ["power", "link", "overlay"], +} + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities @@ -33,6 +61,7 @@ async def async_setup_entry( tado = hass.data[DOMAIN][entry.entry_id][DATA] devices = tado.devices + zones = tado.zones entities = [] # Create device sensors @@ -44,16 +73,30 @@ async def async_setup_entry( entities.extend( [ - TadoDeviceSensor(tado, device, variable) + TadoDeviceBinarySensor(tado, device, variable) for variable in DEVICE_SENSORS[device_type] ] ) + # Create zone sensors + for zone in zones: + zone_type = zone["type"] + if zone_type not in ZONE_SENSORS: + _LOGGER.warning("Unknown zone type skipped: %s", zone_type) + continue + + entities.extend( + [ + TadoZoneBinarySensor(tado, zone["name"], zone["id"], variable) + for variable in ZONE_SENSORS[zone_type] + ] + ) + if entities: async_add_entities(entities, True) -class TadoDeviceSensor(TadoDeviceEntity, BinarySensorEntity): +class TadoDeviceBinarySensor(TadoDeviceEntity, BinarySensorEntity): """Representation of a tado Sensor.""" def __init__(self, tado, device_info, device_variable): @@ -114,10 +157,10 @@ class TadoDeviceSensor(TadoDeviceEntity, BinarySensorEntity): @callback def _async_update_device_data(self): """Handle update callbacks.""" - for device in self._tado.devices: - if device["serialNo"] == self.device_id: - self._device_info = device - break + try: + self._device_info = self._tado.data["device"][self.device_id] + except KeyError: + return if self.device_variable == "battery state": self._state = self._device_info["batteryState"] == "LOW" @@ -125,3 +168,95 @@ class TadoDeviceSensor(TadoDeviceEntity, BinarySensorEntity): self._state = self._device_info.get("connectionState", {}).get( "value", False ) + + +class TadoZoneBinarySensor(TadoZoneEntity, BinarySensorEntity): + """Representation of a tado Sensor.""" + + def __init__(self, tado, zone_name, zone_id, zone_variable): + """Initialize of the Tado Sensor.""" + self._tado = tado + super().__init__(zone_name, tado.home_id, zone_id) + + self.zone_variable = zone_variable + + self._unique_id = f"{zone_variable} {zone_id} {tado.home_id}" + + self._state = None + self._tado_zone_data = None + + async def async_added_to_hass(self): + """Register for sensor updates.""" + + self.async_on_remove( + async_dispatcher_connect( + self.hass, + SIGNAL_TADO_UPDATE_RECEIVED.format( + self._tado.home_id, "zone", self.zone_id + ), + self._async_update_callback, + ) + ) + self._async_update_zone_data() + + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self.zone_name} {self.zone_variable}" + + @property + def is_on(self): + """Return true if sensor is on.""" + return self._state + + @property + def device_class(self): + """Return the class of this sensor.""" + if self.zone_variable == "early start": + return DEVICE_CLASS_POWER + if self.zone_variable == "link": + return DEVICE_CLASS_CONNECTIVITY + if self.zone_variable == "open window": + return DEVICE_CLASS_WINDOW + if self.zone_variable == "overlay": + return DEVICE_CLASS_POWER + if self.zone_variable == "power": + return DEVICE_CLASS_POWER + return None + + @callback + def _async_update_callback(self): + """Update and write state.""" + self._async_update_zone_data() + self.async_write_ha_state() + + @callback + def _async_update_zone_data(self): + """Handle update callbacks.""" + try: + self._tado_zone_data = self._tado.data["zone"][self.zone_id] + except KeyError: + return + + if self.zone_variable == "power": + self._state = self._tado_zone_data.power + + elif self.zone_variable == "link": + self._state = self._tado_zone_data.link + + elif self.zone_variable == "overlay": + self._state = self._tado_zone_data.overlay_active + + elif self.zone_variable == "early start": + self._state = self._tado_zone_data.preparation + + elif self.zone_variable == "open window": + self._state = bool( + self._tado_zone_data.open_window + or self._tado_zone_data.open_window_detected + ) diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 423205f15b2..9547617a36b 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -46,6 +46,8 @@ from .const import ( TADO_SWING_ON, TADO_TO_HA_FAN_MODE_MAP, TADO_TO_HA_HVAC_MODE_MAP, + TADO_TO_HA_OFFSET_MAP, + TEMP_OFFSET, TYPE_AIR_CONDITIONING, TYPE_HEATING, ) @@ -63,6 +65,13 @@ CLIMATE_TIMER_SCHEMA = { vol.Required(ATTR_TEMPERATURE): vol.Coerce(float), } +SERVICE_TEMP_OFFSET = "set_climate_temperature_offset" +ATTR_OFFSET = "offset" + +CLIMATE_TEMP_OFFSET_SCHEMA = { + vol.Required(ATTR_OFFSET, default=0): vol.Coerce(float), +} + async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, async_add_entities @@ -80,6 +89,12 @@ async def async_setup_entry( "set_timer", ) + platform.async_register_entity_service( + SERVICE_TEMP_OFFSET, + CLIMATE_TEMP_OFFSET_SCHEMA, + "set_temp_offset", + ) + if entities: async_add_entities(entities, True) @@ -89,13 +104,15 @@ def _generate_entities(tado): entities = [] for zone in tado.zones: if zone["type"] in [TYPE_HEATING, TYPE_AIR_CONDITIONING]: - entity = create_climate_entity(tado, zone["name"], zone["id"]) + entity = create_climate_entity( + tado, zone["name"], zone["id"], zone["devices"][0] + ) if entity: entities.append(entity) return entities -def create_climate_entity(tado, name: str, zone_id: int): +def create_climate_entity(tado, name: str, zone_id: int, device_info: dict): """Create a Tado climate entity.""" capabilities = tado.get_capabilities(zone_id) _LOGGER.debug("Capabilities for zone %s: %s", zone_id, capabilities) @@ -178,6 +195,7 @@ def create_climate_entity(tado, name: str, zone_id: int): supported_hvac_modes, supported_fan_modes, support_flags, + device_info, ) return entity @@ -200,6 +218,7 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): supported_hvac_modes, supported_fan_modes, support_flags, + device_info, ): """Initialize of Tado climate entity.""" self._tado = tado @@ -208,6 +227,8 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): self.zone_id = zone_id self.zone_type = zone_type self._unique_id = f"{zone_type} {zone_id} {tado.home_id}" + self._device_info = device_info + self._device_id = self._device_info["shortSerialNo"] self._ac_device = zone_type == TYPE_AIR_CONDITIONING self._supported_hvac_modes = supported_hvac_modes @@ -236,6 +257,8 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): self._tado_zone_data = None + self._tado_zone_temp_offset = {} + self._async_update_zone_data() async def async_added_to_hass(self): @@ -362,6 +385,17 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): hvac_mode=CONST_MODE_HEAT, target_temp=temperature, duration=time_period ) + def set_temp_offset(self, offset): + """Set offset on the entity.""" + + _LOGGER.debug( + "Setting temperature offset for device %s setting to (%d)", + self._device_id, + offset, + ) + + self._tado.set_temperature_offset(self._device_id, offset) + def set_temperature(self, **kwargs): """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) @@ -427,6 +461,11 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): return [TADO_SWING_ON, TADO_SWING_OFF] return None + @property + def device_state_attributes(self): + """Return temperature offset.""" + return self._tado_zone_temp_offset + def set_swing_mode(self, swing_mode): """Set swing modes for the device.""" self._control_hvac(swing_mode=swing_mode) @@ -435,6 +474,16 @@ class TadoClimate(TadoZoneEntity, ClimateEntity): def _async_update_zone_data(self): """Load tado data into zone.""" self._tado_zone_data = self._tado.data["zone"][self.zone_id] + # Assign offset values to mapped attributes + for offset_key, attr in TADO_TO_HA_OFFSET_MAP.items(): + if ( + self._device_id in self._tado.data["device"] + and offset_key + in self._tado.data["device"][self._device_id][TEMP_OFFSET] + ): + self._tado_zone_temp_offset[attr] = self._tado.data["device"][ + self._device_id + ][TEMP_OFFSET][offset_key] self._current_tado_fan_speed = self._tado_zone_data.current_fan_speed self._current_tado_hvac_mode = self._tado_zone_data.current_hvac_mode self._current_tado_hvac_action = self._tado_zone_data.current_hvac_action diff --git a/homeassistant/components/tado/config_flow.py b/homeassistant/components/tado/config_flow.py index 0c45cc809af..6c1f06b2626 100644 --- a/homeassistant/components/tado/config_flow.py +++ b/homeassistant/components/tado/config_flow.py @@ -98,12 +98,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(properties["id"]) return await self.async_step_user() - async def async_step_import(self, user_input): - """Handle import.""" - if self._username_already_configured(user_input): - return self.async_abort(reason="already_configured") - return await self.async_step_user(user_input) - def _username_already_configured(self, user_input): """See if we already have a username matching user input configured.""" existing_username = { diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py index 95c524b0433..6e009df7ca2 100644 --- a/homeassistant/components/tado/const.py +++ b/homeassistant/components/tado/const.py @@ -75,7 +75,9 @@ CONST_FAN_HIGH = "HIGH" # When we change the temperature setting, we need an overlay mode -CONST_OVERLAY_TADO_MODE = "TADO_MODE" # wait until tado changes the mode automatic +CONST_OVERLAY_TADO_MODE = ( + "NEXT_TIME_BLOCK" # wait until tado changes the mode automatic +) CONST_OVERLAY_MANUAL = "MANUAL" # the user has change the temperature or mode manually CONST_OVERLAY_TIMER = "TIMER" # the temperature will be reset after a timespan @@ -150,3 +152,15 @@ DEFAULT_NAME = "Tado" TADO_ZONE = "Zone" UPDATE_LISTENER = "update_listener" + +# Constants for Temperature Offset +INSIDE_TEMPERATURE_MEASUREMENT = "INSIDE_TEMPERATURE_MEASUREMENT" +TEMP_OFFSET = "temperatureOffset" +TADO_OFFSET_CELSIUS = "celsius" +HA_OFFSET_CELSIUS = "offset_celsius" +TADO_OFFSET_FAHRENHEIT = "fahrenheit" +HA_OFFSET_FAHRENHEIT = "offset_fahrenheit" +TADO_TO_HA_OFFSET_MAP = { + TADO_OFFSET_CELSIUS: HA_OFFSET_CELSIUS, + TADO_OFFSET_FAHRENHEIT: HA_OFFSET_FAHRENHEIT, +} diff --git a/homeassistant/components/tado/entity.py b/homeassistant/components/tado/entity.py index 03900fdeeb5..e9fefe2848b 100644 --- a/homeassistant/components/tado/entity.py +++ b/homeassistant/components/tado/entity.py @@ -11,8 +11,8 @@ class TadoDeviceEntity(Entity): """Initialize a Tado device.""" super().__init__() self._device_info = device_info - self.device_name = device_info["shortSerialNo"] - self.device_id = device_info["serialNo"] + self.device_name = device_info["serialNo"] + self.device_id = device_info["shortSerialNo"] @property def device_info(self): @@ -40,6 +40,7 @@ class TadoZoneEntity(Entity): super().__init__() self._device_zone_id = f"{home_id}_{zone_id}" self.zone_name = zone_name + self.zone_id = zone_id @property def device_info(self): diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index feff03d0b81..9b166027df3 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -2,7 +2,7 @@ "domain": "tado", "name": "Tado", "documentation": "https://www.home-assistant.io/integrations/tado", - "requirements": ["python-tado==0.8.1"], + "requirements": ["python-tado==0.10.0"], "codeowners": ["@michaelarnauts", "@bdraco"], "config_flow": true, "homekit": { diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index 4e8f69b17c8..6613de82bff 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -2,7 +2,12 @@ import logging from homeassistant.config_entries import ConfigEntry -from homeassistant.const import PERCENTAGE, TEMP_CELSIUS +from homeassistant.const import ( + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + PERCENTAGE, + TEMP_CELSIUS, +) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity @@ -23,25 +28,16 @@ ZONE_SENSORS = { TYPE_HEATING: [ "temperature", "humidity", - "power", - "link", "heating", "tado mode", - "overlay", - "early start", - "open window", ], TYPE_AIR_CONDITIONING: [ "temperature", "humidity", - "power", - "link", "ac", "tado mode", - "overlay", - "open window", ], - TYPE_HOT_WATER: ["power", "link", "tado mode", "overlay"], + TYPE_HOT_WATER: ["tado mode"], } @@ -51,10 +47,10 @@ async def async_setup_entry( """Set up the Tado sensor platform.""" tado = hass.data[DOMAIN][entry.entry_id][DATA] - # Create zone sensors zones = tado.zones entities = [] + # Create zone sensors for zone in zones: zone_type = zone["type"] if zone_type not in ZONE_SENSORS: @@ -80,7 +76,6 @@ class TadoZoneSensor(TadoZoneEntity, Entity): self._tado = tado super().__init__(zone_name, tado.home_id, zone_id) - self.zone_id = zone_id self.zone_variable = zone_variable self._unique_id = f"{zone_variable} {zone_id} {tado.home_id}" @@ -136,12 +131,13 @@ class TadoZoneSensor(TadoZoneEntity, Entity): return None @property - def icon(self): - """Icon for the sensor.""" - if self.zone_variable == "temperature": - return "mdi:thermometer" + def device_class(self): + """Return the device class.""" if self.zone_variable == "humidity": - return "mdi:water-percent" + return DEVICE_CLASS_HUMIDITY + if self.zone_variable == "temperature": + return DEVICE_CLASS_TEMPERATURE + return None @callback def _async_update_callback(self): @@ -172,12 +168,6 @@ class TadoZoneSensor(TadoZoneEntity, Entity): "time": self._tado_zone_data.current_humidity_timestamp } - elif self.zone_variable == "power": - self._state = self._tado_zone_data.power - - elif self.zone_variable == "link": - self._state = self._tado_zone_data.link - elif self.zone_variable == "heating": self._state = self._tado_zone_data.heating_power_percentage self._state_attributes = { @@ -188,26 +178,5 @@ class TadoZoneSensor(TadoZoneEntity, Entity): self._state = self._tado_zone_data.ac_power self._state_attributes = {"time": self._tado_zone_data.ac_power_timestamp} - elif self.zone_variable == "tado bridge status": - self._state = self._tado_zone_data.connection - elif self.zone_variable == "tado mode": self._state = self._tado_zone_data.tado_mode - - elif self.zone_variable == "overlay": - self._state = self._tado_zone_data.overlay_active - self._state_attributes = ( - {"termination": self._tado_zone_data.overlay_termination_type} - if self._tado_zone_data.overlay_active - else {} - ) - - elif self.zone_variable == "early start": - self._state = self._tado_zone_data.preparation - - elif self.zone_variable == "open window": - self._state = bool( - self._tado_zone_data.open_window - or self._tado_zone_data.open_window_detected - ) - self._state_attributes = self._tado_zone_data.open_window_attr diff --git a/homeassistant/components/tado/services.yaml b/homeassistant/components/tado/services.yaml index 864511982a3..c9bba7c0ea8 100644 --- a/homeassistant/components/tado/services.yaml +++ b/homeassistant/components/tado/services.yaml @@ -23,3 +23,13 @@ set_water_heater_timer: temperature: description: Temperature to set heater to example: 25 + +set_climate_temperature_offset: + description: Set the temperature offset of climate entities + fields: + entity_id: + description: Entity ID for the tado component to set the temperature offset + example: climate.heating + offset: + description: Offset you would like, can be to 2 decimal places (depending on your device) positive or negative + example: -1.2 diff --git a/homeassistant/components/tado/translations/de.json b/homeassistant/components/tado/translations/de.json index ffab091f726..9dc410b670e 100644 --- a/homeassistant/components/tado/translations/de.json +++ b/homeassistant/components/tado/translations/de.json @@ -4,7 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_auth": "Ung\u00fcltige Authentifizierung", "no_homes": "Es sind keine Standorte mit diesem Tado-Konto verkn\u00fcpft.", "unknown": "Unerwarteter Fehler" @@ -15,7 +15,7 @@ "password": "Passwort", "username": "Benutzername" }, - "title": "Stellen Sie eine Verbindung zu Ihrem Tado-Konto her" + "title": "Stellen eine Verbindung zu deinem Tado-Konto her" } } }, @@ -23,10 +23,10 @@ "step": { "init": { "data": { - "fallback": "Aktivieren Sie den Fallback-Modus." + "fallback": "Aktivieren den Fallback-Modus." }, "description": "Der Fallback-Modus wechselt beim n\u00e4chsten Zeitplanwechsel nach dem manuellen Anpassen einer Zone zu Smart Schedule.", - "title": "Passen Sie die Tado-Optionen an." + "title": "Passe die Tado-Optionen an." } } } diff --git a/homeassistant/components/tado/translations/tr.json b/homeassistant/components/tado/translations/tr.json new file mode 100644 index 00000000000..09ffbf8a7d1 --- /dev/null +++ b/homeassistant/components/tado/translations/tr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "fallback": "Geri d\u00f6n\u00fc\u015f modunu etkinle\u015ftirin." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tado/translations/uk.json b/homeassistant/components/tado/translations/uk.json new file mode 100644 index 00000000000..f1dcf4d575b --- /dev/null +++ b/homeassistant/components/tado/translations/uk.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "no_homes": "\u0427\u0438 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e \u0431\u0443\u0434\u0438\u043d\u043a\u0456\u0432, \u043f\u043e\u0432'\u044f\u0437\u0430\u043d\u0438\u0445 \u0437 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u043c \u0437\u0430\u043f\u0438\u0441\u043e\u043c.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "Tado" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "fallback": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438 \u0440\u0435\u0436\u0438\u043c Fallback" + }, + "description": "\u0420\u0435\u0436\u0438\u043c Fallback \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043d\u0430 Smart Schedule \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u043e\u0433\u043e \u0440\u0430\u0437\u0443 \u043f\u0456\u0441\u043b\u044f \u0440\u0443\u0447\u043d\u043e\u0433\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0437\u043e\u043d\u0438.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Tado" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tag/translations/uk.json b/homeassistant/components/tag/translations/uk.json new file mode 100644 index 00000000000..fdac700612d --- /dev/null +++ b/homeassistant/components/tag/translations/uk.json @@ -0,0 +1,3 @@ +{ + "title": "Tag" +} \ No newline at end of file diff --git a/homeassistant/components/tasmota/manifest.json b/homeassistant/components/tasmota/manifest.json index 5b298d44ce0..bd48cae8e59 100644 --- a/homeassistant/components/tasmota/manifest.json +++ b/homeassistant/components/tasmota/manifest.json @@ -1,9 +1,9 @@ { "domain": "tasmota", - "name": "Tasmota (beta)", + "name": "Tasmota", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tasmota", - "requirements": ["hatasmota==0.1.6"], + "requirements": ["hatasmota==0.2.7"], "dependencies": ["mqtt"], "mqtt": ["tasmota/discovery/#"], "codeowners": ["@emontnemery"] diff --git a/homeassistant/components/tasmota/translations/de.json b/homeassistant/components/tasmota/translations/de.json new file mode 100644 index 00000000000..30874708839 --- /dev/null +++ b/homeassistant/components/tasmota/translations/de.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "step": { + "config": { + "description": "Bitte die Tasmota-Konfiguration einstellen.", + "title": "Tasmota" + }, + "confirm": { + "description": "M\u00f6chtest du Tasmota einrichten?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/tr.json b/homeassistant/components/tasmota/translations/tr.json new file mode 100644 index 00000000000..a559d0911ee --- /dev/null +++ b/homeassistant/components/tasmota/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "config": { + "description": "L\u00fctfen Tasmota yap\u0131land\u0131rmas\u0131n\u0131 girin.", + "title": "Tasmota" + }, + "confirm": { + "description": "Tasmota'y\u0131 kurmak istiyor musunuz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tasmota/translations/uk.json b/homeassistant/components/tasmota/translations/uk.json new file mode 100644 index 00000000000..6639a9c9626 --- /dev/null +++ b/homeassistant/components/tasmota/translations/uk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "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." + }, + "error": { + "invalid_discovery_topic": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043f\u0440\u0435\u0444\u0456\u043a\u0441 \u0442\u0435\u043c\u0438 \u0430\u0432\u0442\u043e\u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f." + }, + "step": { + "config": { + "data": { + "discovery_prefix": "\u041f\u0440\u0435\u0444\u0456\u043a\u0441 \u0442\u0435\u043c\u0438 \u0430\u0432\u0442\u043e\u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 Tasmota.", + "title": "Tasmota" + }, + "confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Tasmota?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/de.json b/homeassistant/components/tellduslive/translations/de.json index a1f6f595a04..098ad9c17be 100644 --- a/homeassistant/components/tellduslive/translations/de.json +++ b/homeassistant/components/tellduslive/translations/de.json @@ -4,9 +4,12 @@ "already_configured": "Dienst ist bereits konfiguriert", "authorize_url_fail": "Unbekannter Fehler beim Erstellen der Authorisierungs-URL", "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", - "unknown": "Unbekannter Fehler ist aufgetreten", + "unknown": "Unerwarteter Fehler", "unknown_authorize_url_generation": "Beim Generieren einer Authentifizierungs-URL ist ein unbekannter Fehler aufgetreten" }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, "step": { "auth": { "description": "So verkn\u00fcpfest du dein TelldusLive-Konto: \n 1. Klicke auf den Link unten \n 2. Melde dich bei Telldus Live an \n 3. Autorisiere ** {app_name} ** (klicke auf ** Yes **). \n 4. Komme hierher zur\u00fcck und klicke auf ** SUBMIT **. \n\n [Link TelldusLive-Konto]({auth_url})", diff --git a/homeassistant/components/tellduslive/translations/fr.json b/homeassistant/components/tellduslive/translations/fr.json index cde9d9c2c68..ef4d7bc44dd 100644 --- a/homeassistant/components/tellduslive/translations/fr.json +++ b/homeassistant/components/tellduslive/translations/fr.json @@ -4,7 +4,8 @@ "already_configured": "TelldusLive est d\u00e9j\u00e0 configur\u00e9", "authorize_url_fail": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation.", "authorize_url_timeout": "D\u00e9lai de g\u00e9n\u00e9ration de l'URL d'authentification d\u00e9pass\u00e9.", - "unknown": "Une erreur inconnue s'est produite" + "unknown": "Une erreur inconnue s'est produite", + "unknown_authorize_url_generation": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation." }, "error": { "invalid_auth": "Authentification invalide" diff --git a/homeassistant/components/tellduslive/translations/lb.json b/homeassistant/components/tellduslive/translations/lb.json index 5e733c2294d..2b809050677 100644 --- a/homeassistant/components/tellduslive/translations/lb.json +++ b/homeassistant/components/tellduslive/translations/lb.json @@ -4,7 +4,8 @@ "already_configured": "Service ass scho konfigur\u00e9iert", "authorize_url_fail": "Onbekannte Feeler beim gener\u00e9ieren vun der Autorisatiouns URL.", "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", - "unknown": "Onerwaarte Feeler" + "unknown": "Onerwaarte Feeler", + "unknown_authorize_url_generation": "Onbekannte Feeler beim erstellen vun der Authorisatiouns URL." }, "error": { "invalid_auth": "Ong\u00eblteg Authentifikatioun" diff --git a/homeassistant/components/tellduslive/translations/tr.json b/homeassistant/components/tellduslive/translations/tr.json new file mode 100644 index 00000000000..300fad68391 --- /dev/null +++ b/homeassistant/components/tellduslive/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "unknown": "Beklenmeyen hata", + "unknown_authorize_url_generation": "Yetkilendirme url'si olu\u015fturulurken bilinmeyen hata." + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/uk.json b/homeassistant/components/tellduslive/translations/uk.json new file mode 100644 index 00000000000..ff7b3337bb9 --- /dev/null +++ b/homeassistant/components/tellduslive/translations/uk.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant.", + "authorize_url_fail": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430", + "unknown_authorize_url_generation": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "step": { + "auth": { + "description": "\u0414\u043b\u044f \u0442\u043e\u0433\u043e, \u0449\u043e\u0431 \u043f\u0440\u0438\u0432'\u044f\u0437\u0430\u0442\u0438 \u0430\u043a\u0430\u0443\u043d\u0442 Telldus Live:\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u043f\u043e \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044e, \u0437\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u043e\u043c\u0443 \u043d\u0438\u0436\u0447\u0435\n2. \u0423\u0432\u0456\u0439\u0434\u0456\u0442\u044c \u0432 Telldus Live\n3. Authorize ** {app_name} ** (\u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c ** Yes **).\n4. \u041f\u043e\u0432\u0435\u0440\u043d\u0456\u0442\u044c\u0441\u044f \u0441\u044e\u0434\u0438 \u0442\u0430 \u043d\u0430\u0442\u0438\u0441\u043d\u0456\u0442\u044c ** \u041f\u0406\u0414\u0422\u0412\u0415\u0420\u0414\u0418\u0422\u0418 **. \n\n[\u041f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u043d\u0430 Telldus Live]({auth_url})", + "title": "Telldus Live" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u043f\u043e\u0440\u043e\u0436\u043d\u044c\u043e", + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043a\u0456\u043d\u0446\u0435\u0432\u0443 \u0442\u043e\u0447\u043a\u0443." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index a8739a86d70..f039a14d5b3 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -7,7 +7,7 @@ "tf-models-official==2.3.0", "pycocotools==2.0.1", "numpy==1.19.2", - "pillow==7.2.0" + "pillow==8.1.0" ], "codeowners": [] } diff --git a/homeassistant/components/tesla/manifest.json b/homeassistant/components/tesla/manifest.json index f1f4df6edd6..3679c0f74d1 100644 --- a/homeassistant/components/tesla/manifest.json +++ b/homeassistant/components/tesla/manifest.json @@ -4,5 +4,10 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tesla", "requirements": ["teslajsonpy==0.10.4"], - "codeowners": ["@zabuldon", "@alandtse"] + "codeowners": ["@zabuldon", "@alandtse"], + "dhcp": [ + {"hostname":"tesla_*","macaddress":"4CFCAA*"}, + {"hostname":"tesla_*","macaddress":"044EAF*"}, + {"hostname":"tesla_*","macaddress":"98ED5C*"} + ] } diff --git a/homeassistant/components/tesla/translations/de.json b/homeassistant/components/tesla/translations/de.json index 09100c355c2..558209af411 100644 --- a/homeassistant/components/tesla/translations/de.json +++ b/homeassistant/components/tesla/translations/de.json @@ -1,7 +1,9 @@ { "config": { "error": { - "cannot_connect": "Verbindungsfehler" + "already_configured": "Konto wurde bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { "user": { @@ -18,6 +20,7 @@ "step": { "init": { "data": { + "enable_wake_on_start": "Aufwachen des Autos beim Start erzwingen", "scan_interval": "Sekunden zwischen den Scans" } } diff --git a/homeassistant/components/tesla/translations/fr.json b/homeassistant/components/tesla/translations/fr.json index c8efc8b4fb5..6134ff25f6b 100644 --- a/homeassistant/components/tesla/translations/fr.json +++ b/homeassistant/components/tesla/translations/fr.json @@ -1,7 +1,7 @@ { "config": { "error": { - "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9", + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9", "cannot_connect": "\u00c9chec de connexion", "invalid_auth": "Authentification invalide" }, diff --git a/homeassistant/components/tesla/translations/tr.json b/homeassistant/components/tesla/translations/tr.json new file mode 100644 index 00000000000..cf0d144c1ed --- /dev/null +++ b/homeassistant/components/tesla/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "error": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "E-posta" + }, + "description": "L\u00fctfen bilgilerinizi giriniz." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tesla/translations/uk.json b/homeassistant/components/tesla/translations/uk.json new file mode 100644 index 00000000000..90d47ec2ff5 --- /dev/null +++ b/homeassistant/components/tesla/translations/uk.json @@ -0,0 +1,29 @@ +{ + "config": { + "error": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0434\u0430\u043d\u0456 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443.", + "title": "Tesla" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "enable_wake_on_start": "\u041f\u0440\u0438\u043c\u0443\u0441\u043e\u0432\u043e \u0440\u043e\u0437\u0431\u0443\u0434\u0438\u0442\u0438 \u043c\u0430\u0448\u0438\u043d\u0443 \u043f\u0440\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0443", + "scan_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043c\u0456\u0436 \u0441\u043a\u0430\u043d\u0443\u0432\u0430\u043d\u043d\u044f\u043c\u0438 (\u0441\u0435\u043a.)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index d5b19c5094f..652804859da 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -2,7 +2,7 @@ "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": ["pyTibber==0.16.0"], + "requirements": ["pyTibber==0.16.1"], "codeowners": ["@danielhiversen"], "quality_scale": "silver", "config_flow": true diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 1fb291bad5e..bb5ebe8011f 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -102,14 +102,6 @@ class TibberSensorElPrice(TibberSensor): async def async_update(self): """Get the latest data and updates the states.""" now = dt_util.now() - if ( - self._tibber_home.current_price_total - and self._last_updated - and self._last_updated.hour == now.hour - and self._tibber_home.last_data_timestamp - ): - return - if ( not self._tibber_home.last_data_timestamp or (self._tibber_home.last_data_timestamp - now).total_seconds() @@ -119,6 +111,14 @@ class TibberSensorElPrice(TibberSensor): _LOGGER.debug("Asking for new data") await self._fetch_data() + elif ( + self._tibber_home.current_price_total + and self._last_updated + and self._last_updated.hour == now.hour + and self._tibber_home.last_data_timestamp + ): + return + res = self._tibber_home.current_price_data() self._state, price_level, self._last_updated = res self._device_state_attributes["price_level"] = price_level diff --git a/homeassistant/components/tibber/translations/de.json b/homeassistant/components/tibber/translations/de.json index 670f57df8ba..8d49c9d9e61 100644 --- a/homeassistant/components/tibber/translations/de.json +++ b/homeassistant/components/tibber/translations/de.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "Ein Tibber-Konto ist bereits konfiguriert." + "already_configured": "Der Dienst ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_access_token": "Ung\u00fcltiger Zugriffs-Token", "timeout": "Zeit\u00fcberschreitung beim Verbinden mit Tibber" }, diff --git a/homeassistant/components/tibber/translations/tr.json b/homeassistant/components/tibber/translations/tr.json new file mode 100644 index 00000000000..5f8e72986b2 --- /dev/null +++ b/homeassistant/components/tibber/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Hizmet zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_access_token": "Ge\u00e7ersiz eri\u015fim belirteci" + }, + "step": { + "user": { + "data": { + "access_token": "Eri\u015fim Belirteci" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tibber/translations/uk.json b/homeassistant/components/tibber/translations/uk.json new file mode 100644 index 00000000000..b1240116856 --- /dev/null +++ b/homeassistant/components/tibber/translations/uk.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u044f \u0441\u043b\u0443\u0436\u0431\u0430 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u0430 \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_access_token": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443.", + "timeout": "\u0427\u0430\u0441 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u043c\u0438\u043d\u0443\u0432." + }, + "step": { + "user": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443" + }, + "description": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443, \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u0438\u0439 \u043d\u0430 \u0441\u0430\u0439\u0442\u0456 https://developer.tibber.com/settings/accesstoken", + "title": "Tibber" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tile/__init__.py b/homeassistant/components/tile/__init__.py index 6b43761956e..aceed9aa7ee 100644 --- a/homeassistant/components/tile/__init__.py +++ b/homeassistant/components/tile/__init__.py @@ -1,26 +1,23 @@ """The Tile component.""" import asyncio from datetime import timedelta +from functools import partial from pytile import async_login from pytile.errors import SessionExpiredError, TileError -from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME -from homeassistant.core import callback +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client -from homeassistant.helpers.update_coordinator import ( - CoordinatorEntity, - DataUpdateCoordinator, - UpdateFailed, -) +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.util.async_ import gather_with_concurrency -from .const import DATA_COORDINATOR, DOMAIN, LOGGER +from .const import DATA_COORDINATOR, DATA_TILE, DOMAIN, LOGGER PLATFORMS = ["device_tracker"] DEVICE_TYPES = ["PHONE", "TILE"] -DEFAULT_ATTRIBUTION = "Data provided by Tile" -DEFAULT_ICON = "mdi:view-grid" +DEFAULT_INIT_TASK_LIMIT = 2 DEFAULT_UPDATE_INTERVAL = timedelta(minutes=2) CONF_SHOW_INACTIVE = "show_inactive" @@ -28,108 +25,71 @@ CONF_SHOW_INACTIVE = "show_inactive" async def async_setup(hass, config): """Set up the Tile component.""" - hass.data[DOMAIN] = {DATA_COORDINATOR: {}} - + hass.data[DOMAIN] = {DATA_COORDINATOR: {}, DATA_TILE: {}} return True -async def async_setup_entry(hass, config_entry): +async def async_setup_entry(hass, entry): """Set up Tile as config entry.""" + hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id] = {} + hass.data[DOMAIN][DATA_TILE][entry.entry_id] = {} + websession = aiohttp_client.async_get_clientsession(hass) - client = await async_login( - config_entry.data[CONF_USERNAME], - config_entry.data[CONF_PASSWORD], - session=websession, - ) + try: + client = await async_login( + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + session=websession, + ) + hass.data[DOMAIN][DATA_TILE][entry.entry_id] = await client.async_get_tiles() + except TileError as err: + raise ConfigEntryNotReady("Error during integration setup") from err - async def async_update_data(): - """Get new data from the API.""" + async def async_update_tile(tile): + """Update the Tile.""" try: - return await client.tiles.all() + return await tile.async_update() except SessionExpiredError: LOGGER.info("Tile session expired; creating a new one") await client.async_init() except TileError as err: raise UpdateFailed(f"Error while retrieving data: {err}") from err - coordinator = DataUpdateCoordinator( - hass, - LOGGER, - name=config_entry.title, - update_interval=DEFAULT_UPDATE_INTERVAL, - update_method=async_update_data, - ) + coordinator_init_tasks = [] + for tile_uuid, tile in hass.data[DOMAIN][DATA_TILE][entry.entry_id].items(): + coordinator = hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][ + tile_uuid + ] = DataUpdateCoordinator( + hass, + LOGGER, + name=tile.name, + update_interval=DEFAULT_UPDATE_INTERVAL, + update_method=partial(async_update_tile, tile), + ) + coordinator_init_tasks.append(coordinator.async_refresh()) - await coordinator.async_refresh() - hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] = coordinator + await gather_with_concurrency(DEFAULT_INIT_TASK_LIMIT, *coordinator_init_tasks) for component in PLATFORMS: hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) + hass.config_entries.async_forward_entry_setup(entry, component) ) return True -async def async_unload_entry(hass, config_entry): +async def async_unload_entry(hass, entry): """Unload a Tile config entry.""" unload_ok = all( await asyncio.gather( *[ - hass.config_entries.async_forward_entry_unload(config_entry, component) + hass.config_entries.async_forward_entry_unload(entry, component) for component in PLATFORMS ] ) ) if unload_ok: - hass.data[DOMAIN][DATA_COORDINATOR].pop(config_entry.entry_id) + hass.data[DOMAIN][DATA_COORDINATOR].pop(entry.entry_id) return unload_ok - - -class TileEntity(CoordinatorEntity): - """Define a generic Tile entity.""" - - def __init__(self, coordinator): - """Initialize.""" - super().__init__(coordinator) - self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} - self._name = None - self._unique_id = None - - @property - def device_state_attributes(self): - """Return the device state attributes.""" - return self._attrs - - @property - def icon(self): - """Return the icon.""" - return DEFAULT_ICON - - @property - def name(self): - """Return the name.""" - return self._name - - @property - def unique_id(self): - """Return the unique ID of the entity.""" - return self._unique_id - - @callback - def _handle_coordinator_update(self): - """Respond to a DataUpdateCoordinator update.""" - self._update_from_latest_data() - self.async_write_ha_state() - - @callback - def _update_from_latest_data(self): - """Update the entity from the latest data.""" - raise NotImplementedError - - async def async_added_to_hass(self): - """Handle entity which will be added.""" - await super().async_added_to_hass() - self._update_from_latest_data() diff --git a/homeassistant/components/tile/const.py b/homeassistant/components/tile/const.py index 91f5b838642..0f6f0dabb5c 100644 --- a/homeassistant/components/tile/const.py +++ b/homeassistant/components/tile/const.py @@ -4,5 +4,6 @@ import logging DOMAIN = "tile" DATA_COORDINATOR = "coordinator" +DATA_TILE = "tile" LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/tile/device_tracker.py b/homeassistant/components/tile/device_tracker.py index 5b0065b2c4e..ae3852a2b07 100644 --- a/homeassistant/components/tile/device_tracker.py +++ b/homeassistant/components/tile/device_tracker.py @@ -4,10 +4,11 @@ import logging from homeassistant.components.device_tracker.config_entry import TrackerEntity from homeassistant.components.device_tracker.const import SOURCE_TYPE_GPS from homeassistant.config_entries import SOURCE_IMPORT -from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ATTR_ATTRIBUTION, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback +from homeassistant.helpers.update_coordinator import CoordinatorEntity -from . import DATA_COORDINATOR, DOMAIN, TileEntity +from . import DATA_COORDINATOR, DATA_TILE, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -19,17 +20,19 @@ ATTR_RING_STATE = "ring_state" ATTR_VOIP_STATE = "voip_state" ATTR_TILE_NAME = "tile_name" +DEFAULT_ATTRIBUTION = "Data provided by Tile" +DEFAULT_ICON = "mdi:view-grid" -async def async_setup_entry(hass, config_entry, async_add_entities): + +async def async_setup_entry(hass, entry, async_add_entities): """Set up Tile device trackers.""" - coordinator = hass.data[DOMAIN][DATA_COORDINATOR][config_entry.entry_id] - async_add_entities( [ - TileDeviceTracker(coordinator, tile_uuid, tile) - for tile_uuid, tile in coordinator.data.items() - ], - True, + TileDeviceTracker( + hass.data[DOMAIN][DATA_COORDINATOR][entry.entry_id][tile_uuid], tile + ) + for tile_uuid, tile in hass.data[DOMAIN][DATA_TILE][entry.entry_id].items() + ] ) @@ -54,21 +57,19 @@ async def async_setup_scanner(hass, config, async_see, discovery_info=None): return True -class TileDeviceTracker(TileEntity, TrackerEntity): +class TileDeviceTracker(CoordinatorEntity, TrackerEntity): """Representation of a network infrastructure device.""" - def __init__(self, coordinator, tile_uuid, tile): + def __init__(self, coordinator, tile): """Initialize.""" super().__init__(coordinator) - self._name = tile["name"] + self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} self._tile = tile - self._tile_uuid = tile_uuid - self._unique_id = f"tile_{tile_uuid}" @property def available(self): """Return if entity is available.""" - return self.coordinator.last_update_success and not self._tile["is_dead"] + return self.coordinator.last_update_success and not self._tile.dead @property def battery_level(self): @@ -78,53 +79,68 @@ class TileDeviceTracker(TileEntity, TrackerEntity): """ return None + @property + def device_state_attributes(self): + """Return the device state attributes.""" + return self._attrs + + @property + def icon(self): + """Return the icon.""" + return DEFAULT_ICON + @property def location_accuracy(self): """Return the location accuracy of the device. Value in meters. """ - state = self._tile["last_tile_state"] - h_accuracy = state.get("h_accuracy") - v_accuracy = state.get("v_accuracy") - - if h_accuracy is not None and v_accuracy is not None: - return round( - ( - self._tile["last_tile_state"]["h_accuracy"] - + self._tile["last_tile_state"]["v_accuracy"] - ) - / 2 - ) - - if h_accuracy is not None: - return h_accuracy - - if v_accuracy is not None: - return v_accuracy - - return None + return self._tile.accuracy @property def latitude(self) -> float: """Return latitude value of the device.""" - return self._tile["last_tile_state"]["latitude"] + return self._tile.latitude @property def longitude(self) -> float: """Return longitude value of the device.""" - return self._tile["last_tile_state"]["longitude"] + return self._tile.longitude + + @property + def name(self): + """Return the name.""" + return self._tile.name + + @property + def unique_id(self): + """Return the unique ID of the entity.""" + return f"tile_{self._tile.uuid}" @property def source_type(self): """Return the source type, eg gps or router, of the device.""" return SOURCE_TYPE_GPS + @callback + def _handle_coordinator_update(self): + """Respond to a DataUpdateCoordinator update.""" + self._update_from_latest_data() + self.async_write_ha_state() + @callback def _update_from_latest_data(self): """Update the entity from the latest data.""" - self._tile = self.coordinator.data[self._tile_uuid] - self._attrs[ATTR_ALTITUDE] = self._tile["last_tile_state"]["altitude"] - self._attrs[ATTR_IS_LOST] = self._tile["last_tile_state"]["is_lost"] - self._attrs[ATTR_RING_STATE] = self._tile["last_tile_state"]["ring_state"] - self._attrs[ATTR_VOIP_STATE] = self._tile["last_tile_state"]["voip_state"] + self._attrs.update( + { + ATTR_ALTITUDE: self._tile.altitude, + ATTR_IS_LOST: self._tile.lost, + ATTR_RING_STATE: self._tile.ring_state, + ATTR_VOIP_STATE: self._tile.voip_state, + } + ) + + async def async_added_to_hass(self): + """Handle entity which will be added.""" + await super().async_added_to_hass() + self._update_from_latest_data() diff --git a/homeassistant/components/tile/manifest.json b/homeassistant/components/tile/manifest.json index 0f2c84c12e1..854fc663ba2 100644 --- a/homeassistant/components/tile/manifest.json +++ b/homeassistant/components/tile/manifest.json @@ -3,6 +3,6 @@ "name": "Tile", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tile", - "requirements": ["pytile==4.0.0"], + "requirements": ["pytile==5.1.0"], "codeowners": ["@bachya"] } diff --git a/homeassistant/components/tile/translations/de.json b/homeassistant/components/tile/translations/de.json index 59f48253a18..1c2af82aa63 100644 --- a/homeassistant/components/tile/translations/de.json +++ b/homeassistant/components/tile/translations/de.json @@ -3,6 +3,9 @@ "abort": { "already_configured": "Konto ist bereits konfiguriert" }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/tile/translations/tr.json b/homeassistant/components/tile/translations/tr.json new file mode 100644 index 00000000000..8a04e2f4bbf --- /dev/null +++ b/homeassistant/components/tile/translations/tr.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "E-posta" + }, + "title": "Karoyu Yap\u0131land\u0131r" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_inactive": "Etkin Olmayan Karolar\u0131 G\u00f6ster" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tile/translations/uk.json b/homeassistant/components/tile/translations/uk.json new file mode 100644 index 00000000000..dc28164fd93 --- /dev/null +++ b/homeassistant/components/tile/translations/uk.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438" + }, + "title": "Tile" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "show_inactive": "\u041f\u043e\u043a\u0430\u0437\u0430\u0442\u0438 \u043d\u0435\u0430\u043a\u0442\u0438\u0432\u043d\u0456 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457" + }, + "title": "Tile" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/timer/translations/uk.json b/homeassistant/components/timer/translations/uk.json index df690bded93..ce937735406 100644 --- a/homeassistant/components/timer/translations/uk.json +++ b/homeassistant/components/timer/translations/uk.json @@ -1,9 +1,9 @@ { "state": { "_": { - "active": "\u0430\u043a\u0442\u0438\u0432\u043d\u0438\u0439", - "idle": "\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f", - "paused": "\u043d\u0430 \u043f\u0430\u0443\u0437\u0456" + "active": "\u0410\u043a\u0442\u0438\u0432\u043d\u0438\u0439", + "idle": "\u041e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f", + "paused": "\u041f\u0440\u0438\u0437\u0443\u043f\u0438\u043d\u0435\u043d\u043e" } } } \ No newline at end of file diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index f81a4a7249a..978e58c2500 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -39,6 +39,9 @@ from .const import ( PROJECT_ID, PROJECT_NAME, PROJECTS, + REMINDER_DATE, + REMINDER_DATE_LANG, + REMINDER_DATE_STRING, SERVICE_NEW_TASK, START, SUMMARY, @@ -56,6 +59,11 @@ NEW_TASK_SERVICE_SCHEMA = vol.Schema( vol.Exclusive(DUE_DATE_STRING, "due_date"): cv.string, vol.Optional(DUE_DATE_LANG): vol.All(cv.string, vol.In(DUE_DATE_VALID_LANGS)), vol.Exclusive(DUE_DATE, "due_date"): cv.string, + vol.Exclusive(REMINDER_DATE_STRING, "reminder_date"): cv.string, + vol.Optional(REMINDER_DATE_LANG): vol.All( + cv.string, vol.In(DUE_DATE_VALID_LANGS) + ), + vol.Exclusive(REMINDER_DATE, "reminder_date"): cv.string, } ) @@ -181,13 +189,34 @@ def setup_platform(hass, config, add_entities, discovery_info=None): due_date = datetime(due.year, due.month, due.day) # Format it in the manner Todoist expects due_date = dt.as_utc(due_date) - date_format = "%Y-%m-%dT%H:%M" + date_format = "%Y-%m-%dT%H:%M%S" due_date = datetime.strftime(due_date, date_format) _due["date"] = due_date if _due: item.update(due=_due) + _reminder_due: dict = {} + if REMINDER_DATE_STRING in call.data: + _reminder_due["string"] = call.data[REMINDER_DATE_STRING] + + if REMINDER_DATE_LANG in call.data: + _reminder_due["lang"] = call.data[REMINDER_DATE_LANG] + + if REMINDER_DATE in call.data: + due_date = dt.parse_datetime(call.data[REMINDER_DATE]) + if due_date is None: + due = dt.parse_date(call.data[REMINDER_DATE]) + due_date = datetime(due.year, due.month, due.day) + # Format it in the manner Todoist expects + due_date = dt.as_utc(due_date) + date_format = "%Y-%m-%dT%H:%M:%S" + due_date = datetime.strftime(due_date, date_format) + _reminder_due["date"] = due_date + + if _reminder_due: + api.reminders.add(item["id"], due=_reminder_due) + # Commit changes api.commit() _LOGGER.debug("Created Todoist task: %s", call.data[CONTENT]) diff --git a/homeassistant/components/todoist/const.py b/homeassistant/components/todoist/const.py index a1e37bf0292..c83567981af 100644 --- a/homeassistant/components/todoist/const.py +++ b/homeassistant/components/todoist/const.py @@ -24,6 +24,10 @@ DUE = "due" DUE_DATE_STRING = "due_date_string" # Service Call: The language of DUE_DATE_STRING DUE_DATE_LANG = "due_date_lang" +# Service Call: When should user be reminded of this task (in natural language)? +REMINDER_DATE_STRING = "reminder_date_string" +# Service Call: The language of REMINDER_DATE_STRING +REMINDER_DATE_LANG = "reminder_date_lang" # Service Call: The available options of DUE_DATE_LANG DUE_DATE_VALID_LANGS = [ "en", @@ -44,6 +48,8 @@ DUE_DATE_VALID_LANGS = [ # Attribute: When is this task due? # Service Call: When is this task due? DUE_DATE = "due_date" +# Service Call: When should user be reminded of this task? +REMINDER_DATE = "reminder_date" # Attribute: Is this task due today? DUE_TODAY = "due_today" # Calendar Platform: When a calendar event ends diff --git a/homeassistant/components/todoist/services.yaml b/homeassistant/components/todoist/services.yaml index 186b15a18a9..88c057c1ef8 100644 --- a/homeassistant/components/todoist/services.yaml +++ b/homeassistant/components/todoist/services.yaml @@ -20,5 +20,14 @@ new_task: description: The language of due_date_string. example: en due_date: - description: The day this task is due, in format YYYY-MM-DD. + description: The time this task is due, in format YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS, in UTC timezone. example: "2019-10-22" + reminder_date_string: + description: When should user be reminded of this task, in natural language. + example: Tomorrow + reminder_date_lang: + description: The language of reminder_date_string. + example: en + reminder_date: + description: When should user be reminded of this task, in format YYYY-MM-DDTHH:MM:SS, in UTC timezone. + example: "2019-10-22T10:30:00" diff --git a/homeassistant/components/toon/manifest.json b/homeassistant/components/toon/manifest.json index 87398fab302..f8f9fc11012 100644 --- a/homeassistant/components/toon/manifest.json +++ b/homeassistant/components/toon/manifest.json @@ -6,5 +6,6 @@ "requirements": ["toonapi==0.2.0"], "dependencies": ["http"], "after_dependencies": ["cloud"], - "codeowners": ["@frenck"] + "codeowners": ["@frenck"], + "dhcp": [{ "hostname": "eneco-*", "macaddress": "74C63B*" }] } diff --git a/homeassistant/components/toon/translations/de.json b/homeassistant/components/toon/translations/de.json index d9060a719d8..c04f3a5f4bb 100644 --- a/homeassistant/components/toon/translations/de.json +++ b/homeassistant/components/toon/translations/de.json @@ -1,7 +1,10 @@ { "config": { "abort": { + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", "no_agreements": "Dieses Konto hat keine Toon-Anzeigen.", + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url}).", "unknown_authorize_url_generation": "Beim Generieren einer Authentifizierungs-URL ist ein unbekannter Fehler aufgetreten" } } diff --git a/homeassistant/components/toon/translations/fr.json b/homeassistant/components/toon/translations/fr.json index caeed852d0a..3fa6059a58f 100644 --- a/homeassistant/components/toon/translations/fr.json +++ b/homeassistant/components/toon/translations/fr.json @@ -6,7 +6,8 @@ "authorize_url_timeout": "Timout de g\u00e9n\u00e9ration de l'URL d'autorisation.", "missing_configuration": "The composant n'est pas configur\u00e9. Veuillez vous r\u00e9f\u00e9rer \u00e0 la documentation.", "no_agreements": "Ce compte n'a pas d'affichages Toon.", - "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )" + "no_url_available": "Aucune URL disponible. Pour plus d'informations sur cette erreur, [consultez la section d'aide] ( {docs_url} )", + "unknown_authorize_url_generation": "Erreur inconnue lors de la g\u00e9n\u00e9ration d'une URL d'autorisation." }, "step": { "agreement": { diff --git a/homeassistant/components/toon/translations/lb.json b/homeassistant/components/toon/translations/lb.json index 6491c666738..e21dfb0c996 100644 --- a/homeassistant/components/toon/translations/lb.json +++ b/homeassistant/components/toon/translations/lb.json @@ -6,7 +6,8 @@ "authorize_url_timeout": "Z\u00e4itiwwerschraidung beim erstellen vun der Autorisatioun's URL.", "missing_configuration": "Komponent ass net konfigur\u00e9iert. Folleg der Dokumentatioun.", "no_agreements": "D\u00ebse Kont huet keen Toon Ecran.", - "no_url_available": "Keng URL disponibel. Fir Informatiounen iwwert d\u00ebse Feeler, [kuck H\u00ebllef Sektioun]({docs_url})" + "no_url_available": "Keng URL disponibel. Fir Informatiounen iwwert d\u00ebse Feeler, [kuck H\u00ebllef Sektioun]({docs_url})", + "unknown_authorize_url_generation": "Onbekannte Feeler beim erstellen vun der Authorisatiouns URL." }, "step": { "agreement": { diff --git a/homeassistant/components/toon/translations/tr.json b/homeassistant/components/toon/translations/tr.json new file mode 100644 index 00000000000..97765a99a7f --- /dev/null +++ b/homeassistant/components/toon/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Se\u00e7ilen anla\u015fma zaten yap\u0131land\u0131r\u0131lm\u0131\u015f.", + "unknown_authorize_url_generation": "Yetkilendirme url'si olu\u015fturulurken bilinmeyen hata." + }, + "step": { + "agreement": { + "data": { + "agreement": "Anla\u015fma" + }, + "description": "Eklemek istedi\u011finiz anla\u015fma adresini se\u00e7in.", + "title": "Anla\u015fman\u0131z\u0131 se\u00e7in" + }, + "pick_implementation": { + "title": "Kimlik do\u011frulamak i\u00e7in kirac\u0131n\u0131z\u0131 se\u00e7in" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/toon/translations/uk.json b/homeassistant/components/toon/translations/uk.json new file mode 100644 index 00000000000..51aa28f3984 --- /dev/null +++ b/homeassistant/components/toon/translations/uk.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u041e\u0431\u0440\u0430\u043d\u0430 \u0443\u0433\u043e\u0434\u0430 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0430.", + "authorize_url_fail": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", + "no_agreements": "\u0423 \u0446\u044c\u043e\u043c\u0443 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u043c\u0443 \u0437\u0430\u043f\u0438\u0441\u0456 \u043d\u0435\u043c\u0430\u0454 \u0434\u0438\u0441\u043f\u043b\u0435\u0457\u0432 Toon.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443.", + "unknown_authorize_url_generation": "\u041d\u0435\u0432\u0456\u0434\u043e\u043c\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0440\u0438 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457." + }, + "step": { + "agreement": { + "data": { + "agreement": "\u0423\u0433\u043e\u0434\u0430" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0430\u0434\u0440\u0435\u0441\u0443 \u0443\u0433\u043e\u0434\u0438, \u044f\u043a\u0443 \u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438.", + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0412\u0430\u0448\u0443 \u0443\u0433\u043e\u0434\u0443" + }, + "pick_implementation": { + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u0440\u0435\u043d\u0434\u0430\u0440\u044f \u0434\u043b\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/de.json b/homeassistant/components/totalconnect/translations/de.json index 25069635cca..530fef95af2 100644 --- a/homeassistant/components/totalconnect/translations/de.json +++ b/homeassistant/components/totalconnect/translations/de.json @@ -1,7 +1,10 @@ { "config": { "abort": { - "already_configured": "Konto bereits konfiguriert" + "already_configured": "Konto wurde bereits konfiguriert" + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { "user": { diff --git a/homeassistant/components/totalconnect/translations/tr.json b/homeassistant/components/totalconnect/translations/tr.json new file mode 100644 index 00000000000..f941db5ab89 --- /dev/null +++ b/homeassistant/components/totalconnect/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/totalconnect/translations/uk.json b/homeassistant/components/totalconnect/translations/uk.json new file mode 100644 index 00000000000..f34a279d598 --- /dev/null +++ b/homeassistant/components/totalconnect/translations/uk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "Total Connect" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/translations/de.json b/homeassistant/components/tplink/translations/de.json index 64bdfc9bf77..48571158085 100644 --- a/homeassistant/components/tplink/translations/de.json +++ b/homeassistant/components/tplink/translations/de.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Es wurden keine TP-Link-Ger\u00e4te im Netzwerk gefunden.", - "single_instance_allowed": "Es ist nur eine einzige Konfiguration erforderlich." + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "step": { "confirm": { diff --git a/homeassistant/components/tplink/translations/tr.json b/homeassistant/components/tplink/translations/tr.json new file mode 100644 index 00000000000..e8f7a5aaf6d --- /dev/null +++ b/homeassistant/components/tplink/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "confirm": { + "description": "TP-Link ak\u0131ll\u0131 cihazlar\u0131 kurmak istiyor musunuz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tplink/translations/uk.json b/homeassistant/components/tplink/translations/uk.json new file mode 100644 index 00000000000..cfeaf049675 --- /dev/null +++ b/homeassistant/components/tplink/translations/uk.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "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." + }, + "step": { + "confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 TP-Link Smart Home?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/translations/de.json b/homeassistant/components/traccar/translations/de.json index 5d5969b2d51..7e253c1d05f 100644 --- a/homeassistant/components/traccar/translations/de.json +++ b/homeassistant/components/traccar/translations/de.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", + "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." + }, "create_entry": { - "default": "Um Ereignisse an den Heimassistenten zu senden, m\u00fcssen die Webhook-Funktionen in Traccar eingerichtet werden.\n\nVerwende die folgende URL: `{webhook_url}}`\n\nSiehe [die Dokumentation]( {docs_url} ) f\u00fcr weitere Details." + "default": "Um Ereignisse an den Heimassistenten zu senden, m\u00fcssen die Webhook-Funktionen in Traccar eingerichtet werden.\n\nVerwende die folgende URL: `{webhook_url}`\n\nSiehe [Dokumentation]({docs_url}) f\u00fcr weitere Details." }, "step": { "user": { diff --git a/homeassistant/components/traccar/translations/lb.json b/homeassistant/components/traccar/translations/lb.json index d7295252005..9e7d16fec3f 100644 --- a/homeassistant/components/traccar/translations/lb.json +++ b/homeassistant/components/traccar/translations/lb.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech." + "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech.", + "webhook_not_internet_accessible": "Deng Home Assistant Instanz muss iwwert Internet accessibel si fir Webhook Noriichten z'empf\u00e4nken." }, "create_entry": { "default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, muss den Webhook Feature am Traccar ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n\nLiest [Dokumentatioun]({docs_url}) fir w\u00e9ider Informatiounen." diff --git a/homeassistant/components/traccar/translations/tr.json b/homeassistant/components/traccar/translations/tr.json index 7d044949a6e..9a2b1a119cd 100644 --- a/homeassistant/components/traccar/translations/tr.json +++ b/homeassistant/components/traccar/translations/tr.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." + }, "step": { "user": { "title": "Traccar'\u0131 kur" diff --git a/homeassistant/components/traccar/translations/uk.json b/homeassistant/components/traccar/translations/uk.json new file mode 100644 index 00000000000..5bfb1714a79 --- /dev/null +++ b/homeassistant/components/traccar/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "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.", + "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c." + }, + "create_entry": { + "default": "\u0414\u043b\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043a\u0438 \u043f\u043e\u0434\u0456\u0439 \u0432 Home Assistant \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Webhook \u0434\u043b\u044f Traccar. \n\n\u0414\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e: \n\n- URL: `{webhook_url}` \n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457" + }, + "step": { + "user": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Traccar?", + "title": "Traccar" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/translations/de.json b/homeassistant/components/tradfri/translations/de.json index 3e55cb701d5..b1ebb2aff0b 100644 --- a/homeassistant/components/tradfri/translations/de.json +++ b/homeassistant/components/tradfri/translations/de.json @@ -2,10 +2,10 @@ "config": { "abort": { "already_configured": "Bridge ist bereits konfiguriert.", - "already_in_progress": "Der Konfigurationsablauf f\u00fcr die Bridge wird bereits ausgef\u00fchrt." + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt" }, "error": { - "cannot_connect": "Verbindung zum Gateway nicht m\u00f6glich.", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_key": "Registrierung mit angegebenem Schl\u00fcssel fehlgeschlagen. Wenn dies weiterhin geschieht, starte den Gateway neu.", "timeout": "Timeout bei der \u00dcberpr\u00fcfung des Codes." }, diff --git a/homeassistant/components/tradfri/translations/tr.json b/homeassistant/components/tradfri/translations/tr.json new file mode 100644 index 00000000000..e4483536b12 --- /dev/null +++ b/homeassistant/components/tradfri/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "auth": { + "data": { + "host": "Ana Bilgisayar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tradfri/translations/uk.json b/homeassistant/components/tradfri/translations/uk.json index a163a4680e3..abd25d04b6b 100644 --- a/homeassistant/components/tradfri/translations/uk.json +++ b/homeassistant/components/tradfri/translations/uk.json @@ -1,14 +1,22 @@ { "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454." + }, "error": { - "cannot_connect": "\u041d\u0435\u043c\u043e\u0436\u043b\u0438\u0432\u043e \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f \u0434\u043e \u0448\u043b\u044e\u0437\u0443." + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_key": "\u0427\u0438 \u043d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u0443\u0432\u0430\u0442\u0438\u0441\u044f \u0437 \u0432\u043a\u0430\u0437\u0430\u043d\u0438\u043c \u043a\u043b\u044e\u0447\u0435\u043c. \u042f\u043a\u0449\u043e \u0446\u0435 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u044c\u0441\u044f, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u0435\u0440\u0435\u0437\u0430\u0432\u0430\u043d\u0442\u0430\u0436\u0438\u0442\u0438 \u0448\u043b\u044e\u0437.", + "timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0438 \u043a\u043e\u0434\u0443." }, "step": { "auth": { "data": { + "host": "\u0425\u043e\u0441\u0442", "security_code": "\u041a\u043e\u0434 \u0431\u0435\u0437\u043f\u0435\u043a\u0438" }, - "title": "\u0412\u0432\u0435\u0434\u0456\u0442\u044c \u043a\u043e\u0434 \u0431\u0435\u0437\u043f\u0435\u043a\u0438" + "description": "\u041a\u043e\u0434 \u0431\u0435\u0437\u043f\u0435\u043a\u0438 \u043c\u043e\u0436\u043d\u0430 \u0437\u043d\u0430\u0439\u0442\u0438 \u043d\u0430 \u0437\u0430\u0434\u043d\u0456\u0439 \u043f\u0430\u043d\u0435\u043b\u0456 \u0448\u043b\u044e\u0437\u0443.", + "title": "IKEA TR\u00c5DFRI" } } } diff --git a/homeassistant/components/transmission/__init__.py b/homeassistant/components/transmission/__init__.py index 00fc2d1b3b5..d020bfe9745 100644 --- a/homeassistant/components/transmission/__init__.py +++ b/homeassistant/components/transmission/__init__.py @@ -1,6 +1,7 @@ """Support for the Transmission BitTorrent client API.""" from datetime import timedelta import logging +from typing import List import transmissionrpc from transmissionrpc.error import TransmissionError @@ -39,6 +40,8 @@ from .const import ( EVENT_STARTED_TORRENT, SERVICE_ADD_TORRENT, SERVICE_REMOVE_TORRENT, + SERVICE_START_TORRENT, + SERVICE_STOP_TORRENT, ) from .errors import AuthenticationError, CannotConnect, UnknownError @@ -57,6 +60,20 @@ SERVICE_REMOVE_TORRENT_SCHEMA = vol.Schema( } ) +SERVICE_START_TORRENT_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_ID): cv.positive_int, + } +) + +SERVICE_STOP_TORRENT_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_ID): cv.positive_int, + } +) + TRANS_SCHEMA = vol.All( vol.Schema( { @@ -115,6 +132,8 @@ async def async_unload_entry(hass, config_entry): if not hass.data[DOMAIN]: hass.services.async_remove(DOMAIN, SERVICE_ADD_TORRENT) hass.services.async_remove(DOMAIN, SERVICE_REMOVE_TORRENT) + hass.services.async_remove(DOMAIN, SERVICE_START_TORRENT) + hass.services.async_remove(DOMAIN, SERVICE_STOP_TORRENT) return True @@ -152,13 +171,13 @@ class TransmissionClient: """Initialize the Transmission RPC API.""" self.hass = hass self.config_entry = config_entry - self.tm_api = None - self._tm_data = None + self.tm_api = None # type: transmissionrpc.Client + self._tm_data = None # type: TransmissionData self.unsub_timer = None @property - def api(self): - """Return the tm_data object.""" + def api(self) -> "TransmissionData": + """Return the TransmissionData object.""" return self._tm_data async def async_setup(self): @@ -206,6 +225,34 @@ class TransmissionClient: "Could not add torrent: unsupported type or no permission" ) + def start_torrent(service): + """Start torrent.""" + tm_client = None + for entry in self.hass.config_entries.async_entries(DOMAIN): + if entry.data[CONF_NAME] == service.data[CONF_NAME]: + tm_client = self.hass.data[DOMAIN][entry.entry_id] + break + if tm_client is None: + _LOGGER.error("Transmission instance is not found") + return + torrent_id = service.data[CONF_ID] + tm_client.tm_api.start_torrent(torrent_id) + tm_client.api.update() + + def stop_torrent(service): + """Stop torrent.""" + tm_client = None + for entry in self.hass.config_entries.async_entries(DOMAIN): + if entry.data[CONF_NAME] == service.data[CONF_NAME]: + tm_client = self.hass.data[DOMAIN][entry.entry_id] + break + if tm_client is None: + _LOGGER.error("Transmission instance is not found") + return + torrent_id = service.data[CONF_ID] + tm_client.tm_api.stop_torrent(torrent_id) + tm_client.api.update() + def remove_torrent(service): """Remove torrent.""" tm_client = None @@ -232,6 +279,20 @@ class TransmissionClient: schema=SERVICE_REMOVE_TORRENT_SCHEMA, ) + self.hass.services.async_register( + DOMAIN, + SERVICE_START_TORRENT, + start_torrent, + schema=SERVICE_START_TORRENT_SCHEMA, + ) + + self.hass.services.async_register( + DOMAIN, + SERVICE_STOP_TORRENT, + stop_torrent, + schema=SERVICE_STOP_TORRENT_SCHEMA, + ) + self.config_entry.add_update_listener(self.async_options_updated) return True @@ -278,18 +339,18 @@ class TransmissionClient: class TransmissionData: """Get the latest data and update the states.""" - def __init__(self, hass, config, api): + def __init__(self, hass, config, api: transmissionrpc.Client): """Initialize the Transmission RPC API.""" self.hass = hass self.config = config - self.data = None - self.torrents = [] - self.session = None - self.available = True - self._api = api - self.completed_torrents = [] - self.started_torrents = [] - self.all_torrents = [] + self.data = None # type: transmissionrpc.Session + self.available = True # type: bool + self._all_torrents = [] # type: List[transmissionrpc.Torrent] + self._api = api # type: transmissionrpc.Client + self._completed_torrents = [] # type: List[transmissionrpc.Torrent] + self._session = None # type: transmissionrpc.Session + self._started_torrents = [] # type: List[transmissionrpc.Torrent] + self._torrents = [] # type: List[transmissionrpc.Torrent] @property def host(self): @@ -301,12 +362,17 @@ class TransmissionData: """Update signal per transmission entry.""" return f"{DATA_UPDATED}-{self.host}" + @property + def torrents(self) -> List[transmissionrpc.Torrent]: + """Get the list of torrents.""" + return self._torrents + def update(self): """Get the latest data from Transmission instance.""" try: self.data = self._api.session_stats() - self.torrents = self._api.get_torrents() - self.session = self._api.get_session() + self._torrents = self._api.get_torrents() + self._session = self._api.get_session() self.check_completed_torrent() self.check_started_torrent() @@ -321,64 +387,62 @@ class TransmissionData: def init_torrent_list(self): """Initialize torrent lists.""" - self.torrents = self._api.get_torrents() - self.completed_torrents = [ - x.name for x in self.torrents if x.status == "seeding" + self._torrents = self._api.get_torrents() + self._completed_torrents = [ + torrent for torrent in self._torrents if torrent.status == "seeding" ] - self.started_torrents = [ - x.name for x in self.torrents if x.status == "downloading" + self._started_torrents = [ + torrent for torrent in self._torrents if torrent.status == "downloading" ] def check_completed_torrent(self): """Get completed torrent functionality.""" - actual_torrents = self.torrents - actual_completed_torrents = [ - var.name for var in actual_torrents if var.status == "seeding" + current_completed_torrents = [ + torrent for torrent in self._torrents if torrent.status == "seeding" ] - - tmp_completed_torrents = list( - set(actual_completed_torrents).difference(self.completed_torrents) + freshly_completed_torrents = set(current_completed_torrents).difference( + self._completed_torrents ) + self._completed_torrents = current_completed_torrents - for var in tmp_completed_torrents: - self.hass.bus.fire(EVENT_DOWNLOADED_TORRENT, {"name": var}) - - self.completed_torrents = actual_completed_torrents + for torrent in freshly_completed_torrents: + self.hass.bus.fire( + EVENT_DOWNLOADED_TORRENT, {"name": torrent.name, "id": torrent.id} + ) def check_started_torrent(self): """Get started torrent functionality.""" - actual_torrents = self.torrents - actual_started_torrents = [ - var.name for var in actual_torrents if var.status == "downloading" + current_started_torrents = [ + torrent for torrent in self._torrents if torrent.status == "downloading" ] - - tmp_started_torrents = list( - set(actual_started_torrents).difference(self.started_torrents) + freshly_started_torrents = set(current_started_torrents).difference( + self._started_torrents ) + self._started_torrents = current_started_torrents - for var in tmp_started_torrents: - self.hass.bus.fire(EVENT_STARTED_TORRENT, {"name": var}) - self.started_torrents = actual_started_torrents + for torrent in freshly_started_torrents: + self.hass.bus.fire( + EVENT_STARTED_TORRENT, {"name": torrent.name, "id": torrent.id} + ) def check_removed_torrent(self): """Get removed torrent functionality.""" - actual_torrents = self.torrents - actual_all_torrents = [var.name for var in actual_torrents] - - removed_torrents = list(set(self.all_torrents).difference(actual_all_torrents)) - for var in removed_torrents: - self.hass.bus.fire(EVENT_REMOVED_TORRENT, {"name": var}) - self.all_torrents = actual_all_torrents + freshly_removed_torrents = set(self._all_torrents).difference(self._torrents) + self._all_torrents = self._torrents + for torrent in freshly_removed_torrents: + self.hass.bus.fire( + EVENT_REMOVED_TORRENT, {"name": torrent.name, "id": torrent.id} + ) def start_torrents(self): """Start all torrents.""" - if len(self.torrents) <= 0: + if len(self._torrents) <= 0: return self._api.start_all() def stop_torrents(self): """Stop all active torrents.""" - torrent_ids = [torrent.id for torrent in self.torrents] + torrent_ids = [torrent.id for torrent in self._torrents] self._api.stop_torrent(torrent_ids) def set_alt_speed_enabled(self, is_enabled): @@ -387,7 +451,7 @@ class TransmissionData: def get_alt_speed_enabled(self): """Get the alternative speed flag.""" - if self.session is None: + if self._session is None: return None - return self.session.alt_speed_enabled + return self._session.alt_speed_enabled diff --git a/homeassistant/components/transmission/const.py b/homeassistant/components/transmission/const.py index 960fd7a65b4..185148f3bd9 100644 --- a/homeassistant/components/transmission/const.py +++ b/homeassistant/components/transmission/const.py @@ -36,6 +36,8 @@ ATTR_TORRENT = "torrent" SERVICE_ADD_TORRENT = "add_torrent" SERVICE_REMOVE_TORRENT = "remove_torrent" +SERVICE_START_TORRENT = "start_torrent" +SERVICE_STOP_TORRENT = "stop_torrent" DATA_UPDATED = "transmission_data_updated" diff --git a/homeassistant/components/transmission/sensor.py b/homeassistant/components/transmission/sensor.py index ea62de71e8d..2a24b80be16 100644 --- a/homeassistant/components/transmission/sensor.py +++ b/homeassistant/components/transmission/sensor.py @@ -1,9 +1,14 @@ """Support for monitoring the Transmission BitTorrent client API.""" +from typing import List + +from transmissionrpc.torrent import Torrent + from homeassistant.const import CONF_NAME, DATA_RATE_MEGABYTES_PER_SECOND, STATE_IDLE from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity +from . import TransmissionClient from .const import ( CONF_LIMIT, CONF_ORDER, @@ -38,7 +43,7 @@ class TransmissionSensor(Entity): def __init__(self, tm_client, client_name, sensor_name, sub_type=None): """Initialize the sensor.""" - self._tm_client = tm_client + self._tm_client = tm_client # type: TransmissionClient self._client_name = client_name self._name = sensor_name self._sub_type = sub_type @@ -163,7 +168,7 @@ class TransmissionTorrentsSensor(TransmissionSensor): self._state = len(torrents) -def _filter_torrents(torrents, statuses=None): +def _filter_torrents(torrents: List[Torrent], statuses=None) -> List[Torrent]: return [ torrent for torrent in torrents diff --git a/homeassistant/components/transmission/services.yaml b/homeassistant/components/transmission/services.yaml index e8114b680ab..04ac5472d4c 100644 --- a/homeassistant/components/transmission/services.yaml +++ b/homeassistant/components/transmission/services.yaml @@ -20,3 +20,23 @@ remove_torrent: delete_data: description: Delete torrent data example: false + +start_torrent: + description: Start a torrent + fields: + name: + description: Instance name as entered during entry config + example: Transmission + id: + description: ID of a torrent + example: 123 + +stop_torrent: + description: Stop a torrent + fields: + name: + description: Instance name as entered during entry config + example: Transmission + id: + description: ID of a torrent + example: 123 diff --git a/homeassistant/components/transmission/translations/de.json b/homeassistant/components/transmission/translations/de.json index a133cd363e0..2355905d1f7 100644 --- a/homeassistant/components/transmission/translations/de.json +++ b/homeassistant/components/transmission/translations/de.json @@ -1,10 +1,11 @@ { "config": { "abort": { - "already_configured": "Host ist bereits konfiguriert." + "already_configured": "Ger\u00e4t ist bereits konfiguriert" }, "error": { - "cannot_connect": "Verbindung zum Host nicht m\u00f6glich", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "name_exists": "Name existiert bereits" }, "step": { diff --git a/homeassistant/components/transmission/translations/tr.json b/homeassistant/components/transmission/translations/tr.json new file mode 100644 index 00000000000..cffcc65151c --- /dev/null +++ b/homeassistant/components/transmission/translations/tr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "password": "Parola", + "port": "Port", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/transmission/translations/uk.json b/homeassistant/components/transmission/translations/uk.json new file mode 100644 index 00000000000..5bc74f7da2a --- /dev/null +++ b/homeassistant/components/transmission/translations/uk.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "name_exists": "\u0426\u044f \u043d\u0430\u0437\u0432\u0430 \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "Transmission" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "limit": "\u041e\u0431\u043c\u0435\u0436\u0435\u043d\u043d\u044f", + "order": "\u041f\u043e\u0440\u044f\u0434\u043e\u043a", + "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Transmission" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 719f8c52e7a..d278283baaf 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -27,7 +27,6 @@ from homeassistant.const import ( CONF_PLATFORM, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, - HTTP_OK, ) from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError @@ -117,7 +116,7 @@ async def async_setup(hass, config): use_cache = conf.get(CONF_CACHE, DEFAULT_CACHE) cache_dir = conf.get(CONF_CACHE_DIR, DEFAULT_CACHE_DIR) time_memory = conf.get(CONF_TIME_MEMORY, DEFAULT_TIME_MEMORY) - base_url = conf.get(CONF_BASE_URL) or get_url(hass) + base_url = conf.get(CONF_BASE_URL) hass.data[BASE_URL_KEY] = base_url await tts.async_init_cache(use_cache, cache_dir, time_memory, base_url) @@ -165,13 +164,16 @@ async def async_setup(hass, config): options = service.data.get(ATTR_OPTIONS) try: - url = await tts.async_get_url( + url = await tts.async_get_url_path( p_type, message, cache=cache, language=language, options=options ) except HomeAssistantError as err: _LOGGER.error("Error on init TTS: %s", err) return + base = tts.base_url or get_url(hass) + url = base + url + data = { ATTR_MEDIA_CONTENT_ID: url, ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, @@ -290,7 +292,7 @@ class SpeechManager: provider.name = engine self.providers[engine] = provider - async def async_get_url( + async def async_get_url_path( self, engine, message, cache=None, language=None, options=None ): """Get URL for play message. @@ -342,7 +344,7 @@ class SpeechManager: engine, key, message, use_cache, language, options ) - return f"{self.base_url}/api/tts_proxy/{filename}" + return f"/api/tts_proxy/{filename}" async def async_get_tts_audio(self, engine, key, message, cache, language, options): """Receive TTS and store for view in cache. @@ -579,15 +581,17 @@ class TextToSpeechUrlView(HomeAssistantView): options = data.get(ATTR_OPTIONS) try: - url = await self.tts.async_get_url( + path = await self.tts.async_get_url_path( p_type, message, cache=cache, language=language, options=options ) - resp = self.json({"url": url}, HTTP_OK) except HomeAssistantError as err: _LOGGER.error("Error on init tts: %s", err) - resp = self.json({"error": err}, HTTP_BAD_REQUEST) + return self.json({"error": err}, HTTP_BAD_REQUEST) - return resp + base = self.tts.base_url or get_url(self.tts.hass) + url = base + path + + return self.json({"url": url, "path": path}) class TextToSpeechView(HomeAssistantView): @@ -595,7 +599,7 @@ class TextToSpeechView(HomeAssistantView): requires_auth = False url = "/api/tts_proxy/{filename}" - name = "api:tts:speech" + name = "api:tts_speech" def __init__(self, tts): """Initialize a tts view.""" @@ -614,4 +618,4 @@ class TextToSpeechView(HomeAssistantView): def get_base_url(hass): """Get base URL.""" - return hass.data[BASE_URL_KEY] + return hass.data[BASE_URL_KEY] or get_url(hass) diff --git a/homeassistant/components/tuya/climate.py b/homeassistant/components/tuya/climate.py index 6da15b0d29e..da851d4a776 100644 --- a/homeassistant/components/tuya/climate.py +++ b/homeassistant/components/tuya/climate.py @@ -23,8 +23,6 @@ from homeassistant.const import ( ATTR_TEMPERATURE, CONF_PLATFORM, CONF_UNIT_OF_MEASUREMENT, - PRECISION_TENTHS, - PRECISION_WHOLE, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) @@ -154,13 +152,6 @@ class TuyaClimateEntity(TuyaDevice, ClimateEntity): self.operations.append(ha_mode) self._has_operation = True - @property - def precision(self): - """Return the precision of the system.""" - if self._tuya.has_decimal(): - return PRECISION_TENTHS - return PRECISION_WHOLE - @property def temperature_unit(self): """Return the unit of measurement used by the platform.""" diff --git a/homeassistant/components/tuya/translations/de.json b/homeassistant/components/tuya/translations/de.json index 4cdcdfced79..67a61f81a1c 100644 --- a/homeassistant/components/tuya/translations/de.json +++ b/homeassistant/components/tuya/translations/de.json @@ -1,9 +1,13 @@ { "config": { "abort": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, "flow_title": "Tuya Konfiguration", "step": { "user": { @@ -11,7 +15,7 @@ "password": "Passwort", "username": "Benutzername" }, - "description": "Geben Sie Ihre Tuya-Anmeldeinformationen ein.", + "description": "Gib deine Tuya-Anmeldeinformationen ein.", "title": "Tuya" } } diff --git a/homeassistant/components/tuya/translations/lb.json b/homeassistant/components/tuya/translations/lb.json index 884eb328fe4..0000f9ef6e6 100644 --- a/homeassistant/components/tuya/translations/lb.json +++ b/homeassistant/components/tuya/translations/lb.json @@ -23,6 +23,9 @@ } }, "options": { + "abort": { + "cannot_connect": "Feeler beim verbannen" + }, "error": { "dev_multi_type": "Multiple ausgewielte Ger\u00e4ter fir ze konfigur\u00e9ieren musse vum selwechten Typ sinn", "dev_not_config": "Typ vun Apparat net konfigur\u00e9ierbar", diff --git a/homeassistant/components/tuya/translations/tr.json b/homeassistant/components/tuya/translations/tr.json index 5a4de08033c..2edf3276b6c 100644 --- a/homeassistant/components/tuya/translations/tr.json +++ b/homeassistant/components/tuya/translations/tr.json @@ -1,11 +1,39 @@ { + "config": { + "abort": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "flow_title": "Tuya yap\u0131land\u0131rmas\u0131", + "step": { + "user": { + "data": { + "country_code": "Hesap \u00fclke kodunuz (\u00f6r. ABD i\u00e7in 1 veya \u00c7in i\u00e7in 86)", + "password": "Parola", + "platform": "Hesab\u0131n\u0131z\u0131n kay\u0131tl\u0131 oldu\u011fu uygulama", + "username": "Kullan\u0131c\u0131 Ad\u0131" + }, + "description": "Tuya kimlik bilgilerinizi girin.", + "title": "Tuya" + } + } + }, "options": { "abort": { "cannot_connect": "Ba\u011flanma hatas\u0131" }, + "error": { + "dev_not_config": "Cihaz t\u00fcr\u00fc yap\u0131land\u0131r\u0131lamaz", + "dev_not_found": "Cihaz bulunamad\u0131" + }, "step": { "device": { "data": { + "brightness_range_mode": "Cihaz\u0131n kulland\u0131\u011f\u0131 parlakl\u0131k aral\u0131\u011f\u0131", "max_temp": "Maksimum hedef s\u0131cakl\u0131k (varsay\u0131lan olarak min ve maks = 0 kullan\u0131n)", "min_kelvin": "Kelvin destekli min renk s\u0131cakl\u0131\u011f\u0131", "min_temp": "Minimum hedef s\u0131cakl\u0131k (varsay\u0131lan i\u00e7in min ve maks = 0 kullan\u0131n)", diff --git a/homeassistant/components/tuya/translations/uk.json b/homeassistant/components/tuya/translations/uk.json new file mode 100644 index 00000000000..1d2709d260a --- /dev/null +++ b/homeassistant/components/tuya/translations/uk.json @@ -0,0 +1,63 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "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." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "flow_title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Tuya", + "step": { + "user": { + "data": { + "country_code": "\u041a\u043e\u0434 \u043a\u0440\u0430\u0457\u043d\u0438 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u043e\u0433\u043e \u0437\u0430\u043f\u0438\u0441\u0443 (1 \u0434\u043b\u044f \u0421\u0428\u0410 \u0430\u0431\u043e 86 \u0434\u043b\u044f \u041a\u0438\u0442\u0430\u044e)", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "platform": "\u0414\u043e\u0434\u0430\u0442\u043e\u043a, \u0432 \u044f\u043a\u043e\u043c\u0443 \u0437\u0430\u0440\u0435\u0454\u0441\u0442\u0440\u043e\u0432\u0430\u043d\u043e \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 Tuya.", + "title": "Tuya" + } + } + }, + "options": { + "abort": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "error": { + "dev_multi_type": "\u041a\u0456\u043b\u044c\u043a\u0430 \u043e\u0431\u0440\u0430\u043d\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 \u0434\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0431\u0443\u0442\u0438 \u043e\u0434\u043d\u043e\u0433\u043e \u0442\u0438\u043f\u0443.", + "dev_not_config": "\u0422\u0438\u043f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u043d\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f.", + "dev_not_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e." + }, + "step": { + "device": { + "data": { + "brightness_range_mode": "\u0414\u0456\u0430\u043f\u0430\u0437\u043e\u043d \u044f\u0441\u043a\u0440\u0430\u0432\u043e\u0441\u0442\u0456, \u044f\u043a\u0438\u0439 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c", + "curr_temp_divider": "\u0414\u0456\u043b\u044c\u043d\u0438\u043a \u043f\u043e\u0442\u043e\u0447\u043d\u043e\u0433\u043e \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0438 (0 = \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c)", + "max_kelvin": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0430 \u043a\u043e\u043b\u0456\u0440\u043d\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u0432 \u043a\u0435\u043b\u044c\u0432\u0456\u043d\u0430\u0445)", + "max_temp": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430 \u0446\u0456\u043b\u044c\u043e\u0432\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 min \u0456 max = 0)", + "min_kelvin": "\u041c\u0456\u043d\u0456\u043c\u0430\u043b\u044c\u043d\u0430 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0432\u0430\u043d\u0430 \u043a\u043e\u043b\u0456\u0440\u043d\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u0432 \u043a\u0435\u043b\u044c\u0432\u0456\u043d\u0430\u0445)", + "min_temp": "\u041c\u0456\u043d\u0456\u043c\u0430\u043b\u044c\u043d\u0430 \u0446\u0456\u043b\u044c\u043e\u0432\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 (\u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0439\u0442\u0435 min \u0456 max = 0)", + "support_color": "\u041f\u0440\u0438\u043c\u0443\u0441\u043e\u0432\u0430 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u043a\u0430 \u043a\u043e\u043b\u044c\u043e\u0440\u0443", + "temp_divider": "\u0414\u0456\u043b\u044c\u043d\u0438\u043a \u0437\u043d\u0430\u0447\u0435\u043d\u044c \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0438 (0 = \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c)", + "tuya_max_coltemp": "\u041c\u0430\u043a\u0441\u0438\u043c\u0430\u043b\u044c\u043d\u0430 \u043a\u043e\u043b\u0456\u0440\u043d\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430, \u044f\u043a\u0430 \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u044f\u0454\u0442\u044c\u0441\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c", + "unit_of_measurement": "\u041e\u0434\u0438\u043d\u0438\u0446\u044f \u0432\u0438\u043c\u0456\u0440\u0443 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0438, \u044f\u043a\u0430 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c" + }, + "description": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438 \u0434\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0438\u0434\u0438\u043c\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u0434\u043b\u044f {device_type} \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e '{device_name}'", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Tuya" + }, + "init": { + "data": { + "discovery_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0438\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", + "list_devices": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u0434\u043b\u044f \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0430\u0431\u043e \u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c \u0434\u043b\u044f \u0437\u0431\u0435\u0440\u0435\u0436\u0435\u043d\u043d\u044f \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457", + "query_device": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439, \u044f\u043a\u0438\u0439 \u0431\u0443\u0434\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0432\u0430\u0442\u0438 \u043c\u0435\u0442\u043e\u0434 \u0437\u0430\u043f\u0438\u0442\u0443 \u0434\u043b\u044f \u0431\u0456\u043b\u044c\u0448 \u0448\u0432\u0438\u0434\u043a\u043e\u0433\u043e \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f \u0441\u0442\u0430\u0442\u0443\u0441\u0443", + "query_interval": "\u0406\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u043f\u0438\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)" + }, + "description": "\u041d\u0435 \u0432\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u044e\u0439\u0442\u0435 \u0437\u0430\u043d\u0430\u0434\u0442\u043e \u043d\u0438\u0437\u044c\u043a\u0456 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0456\u043d\u0442\u0435\u0440\u0432\u0430\u043b\u0443 \u043e\u043f\u0438\u0442\u0443\u0432\u0430\u043d\u043d\u044f, \u0456\u043d\u0430\u043a\u0448\u0435 \u0432\u0438\u043a\u043b\u0438\u043a\u0438 \u043d\u0435 \u0431\u0443\u0434\u0443\u0442\u044c \u0433\u0435\u043d\u0435\u0440\u0443\u0432\u0430\u0442\u0438 \u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u043d\u044f \u043f\u0440\u043e \u043f\u043e\u043c\u0438\u043b\u043a\u0443 \u0432 \u0436\u0443\u0440\u043d\u0430\u043b\u0456.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f Tuya" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/translations/de.json b/homeassistant/components/twentemilieu/translations/de.json index 27ba9bb29c7..38cabb6c22e 100644 --- a/homeassistant/components/twentemilieu/translations/de.json +++ b/homeassistant/components/twentemilieu/translations/de.json @@ -1,7 +1,10 @@ { "config": { + "abort": { + "already_configured": "Standort ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler", + "cannot_connect": "Verbindung fehlgeschlagen", "invalid_address": "Adresse nicht im Einzugsgebiet von Twente Milieu gefunden." }, "step": { diff --git a/homeassistant/components/twentemilieu/translations/tr.json b/homeassistant/components/twentemilieu/translations/tr.json new file mode 100644 index 00000000000..590aec1894c --- /dev/null +++ b/homeassistant/components/twentemilieu/translations/tr.json @@ -0,0 +1,10 @@ +{ + "config": { + "abort": { + "already_configured": "Konum zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twentemilieu/translations/uk.json b/homeassistant/components/twentemilieu/translations/uk.json new file mode 100644 index 00000000000..435bd79fb85 --- /dev/null +++ b/homeassistant/components/twentemilieu/translations/uk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0434\u043b\u044f \u0446\u044c\u043e\u0433\u043e \u043c\u0456\u0441\u0446\u0435\u0437\u043d\u0430\u0445\u043e\u0434\u0436\u0435\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_address": "\u0410\u0434\u0440\u0435\u0441\u0443 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u043e \u0432 \u0437\u043e\u043d\u0456 \u043e\u0431\u0441\u043b\u0443\u0433\u043e\u0432\u0443\u0432\u0430\u043d\u043d\u044f Twente Milieu." + }, + "step": { + "user": { + "data": { + "house_letter": "\u0414\u043e\u043f\u043e\u0432\u043d\u0435\u043d\u043d\u044f \u0434\u043e \u043d\u043e\u043c\u0435\u0440\u0443 \u0434\u043e\u043c\u0443", + "house_number": "\u041d\u043e\u043c\u0435\u0440 \u0431\u0443\u0434\u0438\u043d\u043a\u0443", + "post_code": "\u041f\u043e\u0448\u0442\u043e\u0432\u0438\u0439 \u0456\u043d\u0434\u0435\u043a\u0441" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Twente Milieu \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0432\u0438\u0432\u0435\u0437\u0435\u043d\u043d\u044f \u0441\u043c\u0456\u0442\u0442\u044f \u0437\u0430 \u0412\u0430\u0448\u043e\u044e \u0430\u0434\u0440\u0435\u0441\u043e\u044e.", + "title": "Twente Milieu" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/translations/de.json b/homeassistant/components/twilio/translations/de.json index 864fee4c238..61df22c10f8 100644 --- a/homeassistant/components/twilio/translations/de.json +++ b/homeassistant/components/twilio/translations/de.json @@ -1,11 +1,15 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich.", + "webhook_not_internet_accessible": "Deine Home Assistant-Instanz muss \u00fcber das Internet erreichbar sein, um Webhook-Nachrichten empfangen zu k\u00f6nnen." + }, "create_entry": { - "default": "Um Ereignisse an den Home Assistant zu senden, musst du [Webhooks mit Twilio]({twilio_url}) einrichten. \n\n F\u00fclle die folgenden Informationen aus: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n - Inhaltstyp: application / x-www-form-urlencoded \n\nLies in der [Dokumentation]({docs_url}) wie du Automationen f\u00fcr die Verarbeitung eingehender Daten konfigurierst." + "default": "Um Ereignisse an Home Assistant zu senden, musst du [Webhooks mit Twilio]({twilio_url}) einrichten. \n\n F\u00fclle die folgenden Informationen aus: \n\n - URL: ` {webhook_url} ` \n - Methode: POST \n - Inhaltstyp: application / x-www-form-urlencoded \n\nLies in der [Dokumentation]({docs_url}), wie du Automationen f\u00fcr die Verarbeitung eingehender Daten konfigurierst." }, "step": { "user": { - "description": "M\u00f6chtest du Twilio wirklich einrichten?", + "description": "M\u00f6chtest du mit der Einrichtung beginnen?", "title": "Twilio-Webhook einrichten" } } diff --git a/homeassistant/components/twilio/translations/lb.json b/homeassistant/components/twilio/translations/lb.json index 2721402c1f3..7889f244c6e 100644 --- a/homeassistant/components/twilio/translations/lb.json +++ b/homeassistant/components/twilio/translations/lb.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech." + "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun ass m\u00e9iglech.", + "webhook_not_internet_accessible": "Deng Home Assistant Instanz muss iwwert Internet accessibel si fir Webhook Noriichten z'empf\u00e4nken." }, "create_entry": { "default": "Fir Evenementer un Home Assistant ze sch\u00e9cken, mussen [Webhooks mat Twilio]({twilio_url}) ageriicht ginn.\n\nF\u00ebllt folgend Informatiounen aus:\n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded\n\nLiest [Dokumentatioun]({docs_url}) w\u00e9i een Automatiounen ariicht welch eingehend Donn\u00e9\u00eb trait\u00e9ieren." diff --git a/homeassistant/components/twilio/translations/tr.json b/homeassistant/components/twilio/translations/tr.json new file mode 100644 index 00000000000..84adcdf8225 --- /dev/null +++ b/homeassistant/components/twilio/translations/tr.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr.", + "webhook_not_internet_accessible": "Webhook mesajlar\u0131n\u0131 alabilmek i\u00e7in Home Assistant \u00f6rne\u011finize internetten eri\u015filebilmelidir." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twilio/translations/uk.json b/homeassistant/components/twilio/translations/uk.json new file mode 100644 index 00000000000..8ea0ce86a37 --- /dev/null +++ b/homeassistant/components/twilio/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "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.", + "webhook_not_internet_accessible": "\u0412\u0430\u0448 Home Assistant \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0439 \u0437 \u0406\u043d\u0442\u0435\u0440\u043d\u0435\u0442\u0443 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f Webhook-\u043f\u043e\u0432\u0456\u0434\u043e\u043c\u043b\u0435\u043d\u044c." + }, + "create_entry": { + "default": "\u0414\u043b\u044f \u0432\u0456\u0434\u043f\u0440\u0430\u0432\u043a\u0438 \u043f\u043e\u0434\u0456\u0439 \u0432 Home Assistant \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Webhook \u0434\u043b\u044f [Twilio]({twilio_url}). \n\n\u0417\u0430\u043f\u043e\u0432\u043d\u0456\u0442\u044c \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u0443 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u044e: \n\n- URL: `{webhook_url}`\n- Method: POST\n- Content Type: application/x-www-form-urlencoded \n\n\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438]({docs_url}) \u0434\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0437\u0430\u0446\u0456\u0439 \u043f\u043e \u043e\u0431\u0440\u043e\u0431\u0446\u0456 \u0434\u0430\u043d\u0438\u0445, \u0449\u043e \u043d\u0430\u0434\u0445\u043e\u0434\u044f\u0442\u044c." + }, + "step": { + "user": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?", + "title": "Twilio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twinkly/translations/de.json b/homeassistant/components/twinkly/translations/de.json index 2b4c70a0bad..c196f53262d 100644 --- a/homeassistant/components/twinkly/translations/de.json +++ b/homeassistant/components/twinkly/translations/de.json @@ -1,7 +1,10 @@ { "config": { + "abort": { + "device_exists": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "user": { diff --git a/homeassistant/components/twinkly/translations/fr.json b/homeassistant/components/twinkly/translations/fr.json new file mode 100644 index 00000000000..5071b7e302a --- /dev/null +++ b/homeassistant/components/twinkly/translations/fr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "device_exists": "D\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "Connexion impossible" + }, + "step": { + "user": { + "data": { + "host": "Nom r\u00e9seau (ou adresse IP) de votre Twinkly" + }, + "description": "Configurer votre Twinkly" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twinkly/translations/lb.json b/homeassistant/components/twinkly/translations/lb.json new file mode 100644 index 00000000000..2e00a8ae4db --- /dev/null +++ b/homeassistant/components/twinkly/translations/lb.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "device_exists": "Apparat ass scho konfigur\u00e9iert" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twinkly/translations/tr.json b/homeassistant/components/twinkly/translations/tr.json index 14365f988bd..d2e7173dad3 100644 --- a/homeassistant/components/twinkly/translations/tr.json +++ b/homeassistant/components/twinkly/translations/tr.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "device_exists": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/twinkly/translations/uk.json b/homeassistant/components/twinkly/translations/uk.json new file mode 100644 index 00000000000..bd256d31b03 --- /dev/null +++ b/homeassistant/components/twinkly/translations/uk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "device_exists": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "host": "\u0406\u043c'\u044f \u0445\u043e\u0441\u0442\u0430 (\u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430) \u0412\u0430\u0448\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Twinkly" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0441\u0432\u0456\u0442\u043b\u043e\u0434\u0456\u043e\u0434\u043d\u043e\u0457 \u0441\u0442\u0440\u0456\u0447\u043a\u0438 Twinkly", + "title": "Twinkly" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/twitter/manifest.json b/homeassistant/components/twitter/manifest.json index c497ebfa6f5..acd47253b82 100644 --- a/homeassistant/components/twitter/manifest.json +++ b/homeassistant/components/twitter/manifest.json @@ -2,6 +2,6 @@ "domain": "twitter", "name": "Twitter", "documentation": "https://www.home-assistant.io/integrations/twitter", - "requirements": ["TwitterAPI==2.6.2.1"], + "requirements": ["TwitterAPI==2.6.3"], "codeowners": [] } diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index f29c3951869..0ea55c15747 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -1,9 +1,11 @@ """Config flow for UniFi.""" import socket +from urllib.parse import urlparse import voluptuous as vol from homeassistant import config_entries +from homeassistant.components import ssdp from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -13,6 +15,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import format_mac from .const import ( CONF_ALLOW_BANDWIDTH_SENSORS, @@ -42,6 +45,12 @@ DEFAULT_SITE_ID = "default" DEFAULT_VERIFY_SSL = False +MODEL_PORTS = { + "UniFi Dream Machine": 443, + "UniFi Dream Machine Pro": 443, +} + + @callback def get_controller_id_from_config_entry(config_entry): """Return controller with a matching bridge id.""" @@ -65,9 +74,11 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): def __init__(self): """Initialize the UniFi flow.""" - self.config = None - self.desc = None + self.config = {} self.sites = None + self.reauth_config_entry = {} + self.reauth_config = {} + self.reauth_schema = {} async def async_step_user(self, user_input=None): """Handle a flow initialized by the user.""" @@ -87,7 +98,13 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): controller = await get_controller(self.hass, **self.config) - self.sites = await controller.sites() + sites = await controller.sites() + self.sites = {site["name"]: site["desc"] for site in sites.values()} + + if self.reauth_config.get(CONF_SITE_ID) in self.sites: + return await self.async_step_site( + {CONF_SITE_ID: self.reauth_config[CONF_SITE_ID]} + ) return await self.async_step_site() @@ -104,21 +121,23 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): ) return self.async_abort(reason="unknown") - host = "" - if await async_discover_unifi(self.hass): + host = self.config.get(CONF_HOST) + if not host and await async_discover_unifi(self.hass): host = "unifi" + data = self.reauth_schema or { + vol.Required(CONF_HOST, default=host): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional( + CONF_PORT, default=self.config.get(CONF_PORT, DEFAULT_PORT) + ): int, + vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool, + } + return self.async_show_form( step_id="user", - data_schema=vol.Schema( - { - vol.Required(CONF_HOST, default=host): str, - vol.Required(CONF_USERNAME): str, - vol.Required(CONF_PASSWORD): str, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, - vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool, - } - ), + data_schema=vol.Schema(data), errors=errors, ) @@ -128,12 +147,17 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): if user_input is not None: try: - desc = user_input.get(CONF_SITE_ID, self.desc) + self.config[CONF_SITE_ID] = user_input[CONF_SITE_ID] + data = {CONF_CONTROLLER: self.config} - for site in self.sites.values(): - if desc == site["desc"]: - self.config[CONF_SITE_ID] = site["name"] - break + if self.reauth_config_entry: + self.hass.config_entries.async_update_entry( + self.reauth_config_entry, data=data + ) + await self.hass.config_entries.async_reload( + self.reauth_config_entry.entry_id + ) + return self.async_abort(reason="reauth_successful") for entry in self._async_current_entries(): controller = entry.data[CONF_CONTROLLER] @@ -143,27 +167,81 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): ): raise AlreadyConfigured - data = {CONF_CONTROLLER: self.config} - - return self.async_create_entry(title=desc, data=data) + site_nice_name = self.sites[self.config[CONF_SITE_ID]] + return self.async_create_entry(title=site_nice_name, data=data) except AlreadyConfigured: return self.async_abort(reason="already_configured") if len(self.sites) == 1: - self.desc = next(iter(self.sites.values()))["desc"] - return await self.async_step_site(user_input={}) - - sites = [] - for site in self.sites.values(): - sites.append(site["desc"]) + return await self.async_step_site({CONF_SITE_ID: next(iter(self.sites))}) return self.async_show_form( step_id="site", - data_schema=vol.Schema({vol.Required(CONF_SITE_ID): vol.In(sites)}), + data_schema=vol.Schema({vol.Required(CONF_SITE_ID): vol.In(self.sites)}), errors=errors, ) + async def async_step_reauth(self, config_entry: dict): + """Trigger a reauthentication flow.""" + self.reauth_config_entry = config_entry + self.reauth_config = config_entry.data[CONF_CONTROLLER] + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context["title_placeholders"] = { + CONF_HOST: self.reauth_config[CONF_HOST], + CONF_SITE_ID: config_entry.title, + } + + self.reauth_schema = { + vol.Required(CONF_HOST, default=self.reauth_config[CONF_HOST]): str, + vol.Required(CONF_USERNAME, default=self.reauth_config[CONF_USERNAME]): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_PORT, default=self.reauth_config[CONF_PORT]): int, + vol.Required( + CONF_VERIFY_SSL, default=self.reauth_config[CONF_VERIFY_SSL] + ): bool, + } + + return await self.async_step_user() + + async def async_step_ssdp(self, discovery_info): + """Handle a discovered unifi device.""" + parsed_url = urlparse(discovery_info[ssdp.ATTR_SSDP_LOCATION]) + model_description = discovery_info[ssdp.ATTR_UPNP_MODEL_DESCRIPTION] + mac_address = format_mac(discovery_info[ssdp.ATTR_UPNP_SERIAL]) + + self.config = { + CONF_HOST: parsed_url.hostname, + } + + if self._host_already_configured(self.config[CONF_HOST]): + return self.async_abort(reason="already_configured") + + await self.async_set_unique_id(mac_address) + self._abort_if_unique_id_configured(updates={CONF_HOST: self.config[CONF_HOST]}) + + # pylint: disable=no-member + self.context["title_placeholders"] = { + CONF_HOST: self.config[CONF_HOST], + CONF_SITE_ID: "default", + } + + port = MODEL_PORTS.get(model_description) + if port is not None: + self.config[CONF_PORT] = port + + return await self.async_step_user() + + def _host_already_configured(self, host): + """See if we already have a unifi entry matching the host.""" + for entry in self._async_current_entries(): + if not entry.data or CONF_CONTROLLER not in entry.data: + continue + if entry.data[CONF_CONTROLLER][CONF_HOST] == host: + return True + return False + class UnifiOptionsFlowHandler(config_entries.OptionsFlow): """Handle Unifi options.""" diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 30b82c65c85..11e02d60a3f 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -1,7 +1,8 @@ """UniFi Controller abstraction.""" import asyncio -from datetime import timedelta +from datetime import datetime, timedelta import ssl +from typing import Optional from aiohttp import CookieJar import aiounifi @@ -27,11 +28,14 @@ import async_timeout from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import CONF_HOST from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval +import homeassistant.util.dt as dt_util from .const import ( CONF_ALLOW_BANDWIDTH_SENSORS, @@ -64,6 +68,7 @@ from .const import ( from .errors import AuthenticationRequired, CannotConnect RETRY_TIMER = 15 +CHECK_HEARTBEAT_INTERVAL = timedelta(seconds=1) SUPPORTED_PLATFORMS = [TRACKER_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN] CLIENT_CONNECTED = ( @@ -94,8 +99,64 @@ class UniFiController: self._site_name = None self._site_role = None + self._cancel_heartbeat_check = None + self._heartbeat_dispatch = {} + self._heartbeat_time = {} + + self.load_config_entry_options() + self.entities = {} + def load_config_entry_options(self): + """Store attributes to avoid property call overhead since they are called frequently.""" + # Device tracker options + options = self.config_entry.options + + # Config entry option to not track clients. + self.option_track_clients = options.get( + CONF_TRACK_CLIENTS, DEFAULT_TRACK_CLIENTS + ) + # Config entry option to not track wired clients. + self.option_track_wired_clients = options.get( + CONF_TRACK_WIRED_CLIENTS, DEFAULT_TRACK_WIRED_CLIENTS + ) + # Config entry option to not track devices. + self.option_track_devices = options.get( + CONF_TRACK_DEVICES, DEFAULT_TRACK_DEVICES + ) + # Config entry option listing what SSIDs are being used to track clients. + self.option_ssid_filter = set(options.get(CONF_SSID_FILTER, [])) + # Config entry option defining number of seconds from last seen to away + self.option_detection_time = timedelta( + seconds=options.get(CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME) + ) + # Config entry option to ignore wired bug. + self.option_ignore_wired_bug = options.get( + CONF_IGNORE_WIRED_BUG, DEFAULT_IGNORE_WIRED_BUG + ) + + # Client control options + + # Config entry option to control poe clients. + self.option_poe_clients = options.get(CONF_POE_CLIENTS, DEFAULT_POE_CLIENTS) + # Config entry option with list of clients to control network access. + self.option_block_clients = options.get(CONF_BLOCK_CLIENT, []) + # Config entry option to control DPI restriction groups. + self.option_dpi_restrictions = options.get( + CONF_DPI_RESTRICTIONS, DEFAULT_DPI_RESTRICTIONS + ) + + # Statistics sensor options + + # Config entry option to allow bandwidth sensors. + self.option_allow_bandwidth_sensors = options.get( + CONF_ALLOW_BANDWIDTH_SENSORS, DEFAULT_ALLOW_BANDWIDTH_SENSORS + ) + # Config entry option to allow uptime sensors. + self.option_allow_uptime_sensors = options.get( + CONF_ALLOW_UPTIME_SENSORS, DEFAULT_ALLOW_UPTIME_SENSORS + ) + @property def controller_id(self): """Return the controller ID.""" @@ -129,81 +190,6 @@ class UniFiController: return client.mac return None - # Device tracker options - - @property - def option_track_clients(self): - """Config entry option to not track clients.""" - return self.config_entry.options.get(CONF_TRACK_CLIENTS, DEFAULT_TRACK_CLIENTS) - - @property - def option_track_wired_clients(self): - """Config entry option to not track wired clients.""" - return self.config_entry.options.get( - CONF_TRACK_WIRED_CLIENTS, DEFAULT_TRACK_WIRED_CLIENTS - ) - - @property - def option_track_devices(self): - """Config entry option to not track devices.""" - return self.config_entry.options.get(CONF_TRACK_DEVICES, DEFAULT_TRACK_DEVICES) - - @property - def option_ssid_filter(self): - """Config entry option listing what SSIDs are being used to track clients.""" - return self.config_entry.options.get(CONF_SSID_FILTER, []) - - @property - def option_detection_time(self): - """Config entry option defining number of seconds from last seen to away.""" - return timedelta( - seconds=self.config_entry.options.get( - CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME - ) - ) - - @property - def option_ignore_wired_bug(self): - """Config entry option to ignore wired bug.""" - return self.config_entry.options.get( - CONF_IGNORE_WIRED_BUG, DEFAULT_IGNORE_WIRED_BUG - ) - - # Client control options - - @property - def option_poe_clients(self): - """Config entry option to control poe clients.""" - return self.config_entry.options.get(CONF_POE_CLIENTS, DEFAULT_POE_CLIENTS) - - @property - def option_block_clients(self): - """Config entry option with list of clients to control network access.""" - return self.config_entry.options.get(CONF_BLOCK_CLIENT, []) - - @property - def option_dpi_restrictions(self): - """Config entry option to control DPI restriction groups.""" - return self.config_entry.options.get( - CONF_DPI_RESTRICTIONS, DEFAULT_DPI_RESTRICTIONS - ) - - # Statistics sensor options - - @property - def option_allow_bandwidth_sensors(self): - """Config entry option to allow bandwidth sensors.""" - return self.config_entry.options.get( - CONF_ALLOW_BANDWIDTH_SENSORS, DEFAULT_ALLOW_BANDWIDTH_SENSORS - ) - - @property - def option_allow_uptime_sensors(self): - """Config entry option to allow uptime sensors.""" - return self.config_entry.options.get( - CONF_ALLOW_UPTIME_SENSORS, DEFAULT_ALLOW_UPTIME_SENSORS - ) - @callback def async_unifi_signalling_callback(self, signal, data): """Handle messages back from UniFi library.""" @@ -291,6 +277,11 @@ class UniFiController: """Event specific per UniFi entry to signal new options.""" return f"unifi-options-{self.controller_id}" + @property + def signal_heartbeat_missed(self): + """Event specific per UniFi device tracker to signal new heartbeat missed.""" + return "unifi-heartbeat-missed" + def update_wireless_clients(self): """Update set of known to be wireless clients.""" new_wireless_clients = set() @@ -330,8 +321,14 @@ class UniFiController: except CannotConnect as err: raise ConfigEntryNotReady from err - except Exception as err: # pylint: disable=broad-except - LOGGER.error("Unknown error connecting with UniFi controller: %s", err) + except AuthenticationRequired: + self.hass.async_create_task( + self.hass.config_entries.flow.async_init( + UNIFI_DOMAIN, + context={"source": SOURCE_REAUTH}, + data=self.config_entry, + ) + ) return False # Restore clients that is not a part of active clients list. @@ -375,12 +372,46 @@ class UniFiController: self.config_entry.add_update_listener(self.async_config_entry_updated) + self._cancel_heartbeat_check = async_track_time_interval( + self.hass, self._async_check_for_stale, CHECK_HEARTBEAT_INTERVAL + ) + return True + @callback + def async_heartbeat( + self, unique_id: str, heartbeat_expire_time: Optional[datetime] = None + ) -> None: + """Signal when a device has fresh home state.""" + if heartbeat_expire_time is not None: + self._heartbeat_time[unique_id] = heartbeat_expire_time + return + + if unique_id in self._heartbeat_time: + del self._heartbeat_time[unique_id] + + @callback + def _async_check_for_stale(self, *_) -> None: + """Check for any devices scheduled to be marked disconnected.""" + now = dt_util.utcnow() + + for unique_id, heartbeat_expire_time in self._heartbeat_time.items(): + if now > heartbeat_expire_time: + async_dispatcher_send( + self.hass, f"{self.signal_heartbeat_missed}_{unique_id}" + ) + @staticmethod async def async_config_entry_updated(hass, config_entry) -> None: - """Handle signals of config entry being updated.""" + """Handle signals of config entry being updated. + + If config entry is updated due to reauth flow + the entry might already have been reset and thus is not available. + """ + if config_entry.entry_id not in hass.data[UNIFI_DOMAIN]: + return controller = hass.data[UNIFI_DOMAIN][config_entry.entry_id] + controller.load_config_entry_options() async_dispatcher_send(hass, controller.signal_options_update) @callback @@ -430,6 +461,10 @@ class UniFiController: unsub_dispatcher() self.listeners = [] + if self._cancel_heartbeat_check: + self._cancel_heartbeat_check() + self._cancel_heartbeat_check = None + return True @@ -466,7 +501,7 @@ async def get_controller( return controller except aiounifi.Unauthorized as err: - LOGGER.warning("Connected to UniFi at %s but not registered.", host) + LOGGER.warning("Connected to UniFi at %s but not registered: %s", host, err) raise AuthenticationRequired from err except ( @@ -475,9 +510,13 @@ async def get_controller( aiounifi.ServiceUnavailable, aiounifi.RequestError, ) as err: - LOGGER.error("Error connecting to the UniFi controller at %s", host) + LOGGER.error("Error connecting to the UniFi controller at %s: %s", host, err) raise CannotConnect from err - except aiounifi.AiounifiException as err: - LOGGER.exception("Unknown UniFi communication error occurred") + except aiounifi.LoginRequired as err: + LOGGER.warning("Connected to UniFi at %s but login required: %s", host, err) + raise AuthenticationRequired from err + + except aiounifi.AiounifiException as err: + LOGGER.exception("Unknown UniFi communication error occurred: %s", err) raise AuthenticationRequired from err diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index a3352631885..6a4d986d5b2 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -21,7 +21,6 @@ from homeassistant.components.device_tracker.const import SOURCE_TYPE_ROUTER from homeassistant.core import callback from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.event import async_track_point_in_utc_time import homeassistant.util.dt as dt_util from .const import ATTR_MANUFACTURER, DOMAIN as UNIFI_DOMAIN @@ -53,6 +52,9 @@ CLIENT_STATIC_ATTRIBUTES = [ "oui", ] + +CLIENT_CONNECTED_ALL_ATTRIBUTES = CLIENT_CONNECTED_ATTRIBUTES + CLIENT_STATIC_ATTRIBUTES + DEVICE_UPGRADED = (ACCESS_POINT_UPGRADED, GATEWAY_UPGRADED, SWITCH_UPGRADED) WIRED_CONNECTION = (WIRED_CLIENT_CONNECTED,) @@ -141,9 +143,9 @@ class UniFiClientTracker(UniFiClient, ScannerEntity): """Set up tracked client.""" super().__init__(client, controller) - self.schedule_update = False - self.cancel_scheduled_update = None + self.heartbeat_check = False self._is_connected = False + if client.last_seen: self._is_connected = ( self.is_wired == client.is_wired @@ -151,31 +153,53 @@ class UniFiClientTracker(UniFiClient, ScannerEntity): - dt_util.utc_from_timestamp(float(client.last_seen)) < controller.option_detection_time ) - if self._is_connected: - self.schedule_update = True + + self.schedule_update = self._is_connected + + async def async_added_to_hass(self) -> None: + """Watch object when added.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{self.controller.signal_heartbeat_missed}_{self.unique_id}", + self._make_disconnected, + ) + ) + await super().async_added_to_hass() async def async_will_remove_from_hass(self) -> None: """Disconnect object when removed.""" - if self.cancel_scheduled_update: - self.cancel_scheduled_update() + self.controller.async_heartbeat(self.unique_id) await super().async_will_remove_from_hass() @callback - def async_update_callback(self) -> None: + def async_signal_reachable_callback(self) -> None: + """Call when controller connection state change.""" + self.async_update_callback(controller_state_change=True) + + # pylint: disable=arguments-differ + @callback + def async_update_callback(self, controller_state_change: bool = False) -> None: """Update the clients state.""" - if self.client.last_updated == SOURCE_EVENT: + if controller_state_change: + if self.controller.available: + self.schedule_update = True + + else: + self.controller.async_heartbeat(self.unique_id) + + elif self.client.last_updated == SOURCE_EVENT: if (self.is_wired and self.client.event.event in WIRED_CONNECTION) or ( not self.is_wired and self.client.event.event in WIRELESS_CONNECTION ): self._is_connected = True self.schedule_update = False - if self.cancel_scheduled_update: - self.cancel_scheduled_update() - self.cancel_scheduled_update = None + self.controller.async_heartbeat(self.unique_id) + self.heartbeat_check = False # Ignore extra scheduled update from wired bug - elif not self.cancel_scheduled_update: + elif not self.heartbeat_check: self.schedule_update = True elif not self.client.event and self.client.last_updated == SOURCE_DATA: @@ -185,23 +209,17 @@ class UniFiClientTracker(UniFiClient, ScannerEntity): if self.schedule_update: self.schedule_update = False - - if self.cancel_scheduled_update: - self.cancel_scheduled_update() - - self.cancel_scheduled_update = async_track_point_in_utc_time( - self.hass, - self._make_disconnected, - dt_util.utcnow() + self.controller.option_detection_time, + self.controller.async_heartbeat( + self.unique_id, dt_util.utcnow() + self.controller.option_detection_time ) + self.heartbeat_check = True super().async_update_callback() @callback - def _make_disconnected(self, _): - """Mark client as disconnected.""" + def _make_disconnected(self, *_): + """No heart beat by device.""" self._is_connected = False - self.cancel_scheduled_update = None self.async_write_ha_state() @property @@ -230,19 +248,34 @@ class UniFiClientTracker(UniFiClient, ScannerEntity): @property def device_state_attributes(self): """Return the client state attributes.""" - attributes = {"is_wired": self.is_wired} + raw = self.client.raw if self.is_connected: - for variable in CLIENT_CONNECTED_ATTRIBUTES: - if variable in self.client.raw: - attributes[variable] = self.client.raw[variable] + attributes = { + k: raw[k] for k in CLIENT_CONNECTED_ALL_ATTRIBUTES if k in raw + } + else: + attributes = {k: raw[k] for k in CLIENT_STATIC_ATTRIBUTES if k in raw} - for variable in CLIENT_STATIC_ATTRIBUTES: - if variable in self.client.raw: - attributes[variable] = self.client.raw[variable] + attributes["is_wired"] = self.is_wired return attributes + @property + def ip_address(self) -> str: + """Return the primary ip address of the device.""" + return self.client.raw.get("ip") + + @property + def mac_address(self) -> str: + """Return the mac address of the device.""" + return self.client.raw.get("mac") + + @property + def hostname(self) -> str: + """Return hostname of the device.""" + return self.client.raw.get("hostname") + async def options_updated(self) -> None: """Config entry options are updated, remove entity if option is disabled.""" if not self.controller.option_track_clients: @@ -269,36 +302,47 @@ class UniFiDeviceTracker(UniFiBase, ScannerEntity): """Set up tracked device.""" super().__init__(device, controller) + self.device = self._item self._is_connected = device.state == 1 - self.cancel_scheduled_update = None + self.schedule_update = False - @property - def device(self): - """Wrap item.""" - return self._item + async def async_added_to_hass(self) -> None: + """Watch object when added.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{self.controller.signal_heartbeat_missed}_{self.unique_id}", + self._make_disconnected, + ) + ) + await super().async_added_to_hass() async def async_will_remove_from_hass(self) -> None: - """Disconnect device object when removed.""" - if self.cancel_scheduled_update: - self.cancel_scheduled_update() + """Disconnect object when removed.""" + self.controller.async_heartbeat(self.unique_id) await super().async_will_remove_from_hass() @callback - def async_update_callback(self): + def async_signal_reachable_callback(self) -> None: + """Call when controller connection state change.""" + self.async_update_callback(controller_state_change=True) + + # pylint: disable=arguments-differ + @callback + def async_update_callback(self, controller_state_change: bool = False) -> None: """Update the devices' state.""" - if self.device.last_updated == SOURCE_DATA: + if controller_state_change: + if self.controller.available: + if self._is_connected: + self.schedule_update = True + else: + self.controller.async_heartbeat(self.unique_id) + + elif self.device.last_updated == SOURCE_DATA: self._is_connected = True - - if self.cancel_scheduled_update: - self.cancel_scheduled_update() - - self.cancel_scheduled_update = async_track_point_in_utc_time( - self.hass, - self._no_heartbeat, - dt_util.utcnow() + timedelta(seconds=self.device.next_interval + 60), - ) + self.schedule_update = True elif ( self.device.last_updated == SOURCE_EVENT @@ -307,13 +351,19 @@ class UniFiDeviceTracker(UniFiBase, ScannerEntity): self.hass.async_create_task(self.async_update_device_registry()) return + if self.schedule_update: + self.schedule_update = False + self.controller.async_heartbeat( + self.unique_id, + dt_util.utcnow() + timedelta(seconds=self.device.next_interval + 60), + ) + super().async_update_callback() @callback - def _no_heartbeat(self, _): + def _make_disconnected(self, *_): """No heart beat by device.""" self._is_connected = False - self.cancel_scheduled_update = None self.async_write_ha_state() @property diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 94b1c90f4f3..cec2d0f859b 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -5,5 +5,17 @@ "documentation": "https://www.home-assistant.io/integrations/unifi", "requirements": ["aiounifi==26"], "codeowners": ["@Kane610"], - "quality_scale": "platinum" + "quality_scale": "platinum", + "ssdp": [ + { + "manufacturer": "Ubiquiti Networks", + "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", + "modelDescription": "UniFi Dream Machine" + }, + { + "manufacturer": "Ubiquiti Networks", + "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", + "modelDescription": "UniFi Dream Machine Pro" + } + ] } diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json index 75e23ae2ed1..15cc2fb45e7 100644 --- a/homeassistant/components/unifi/strings.json +++ b/homeassistant/components/unifi/strings.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "UniFi Network {site} ({host})", "step": { "user": { "title": "Set up UniFi Controller", @@ -19,7 +20,8 @@ "unknown_client_mac": "No client available on that MAC address" }, "abort": { - "already_configured": "Controller site is already configured" + "already_configured": "Controller site is already configured", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } }, "options": { diff --git a/homeassistant/components/unifi/translations/ca.json b/homeassistant/components/unifi/translations/ca.json index a07c034fe12..f1cf4a6349b 100644 --- a/homeassistant/components/unifi/translations/ca.json +++ b/homeassistant/components/unifi/translations/ca.json @@ -1,13 +1,16 @@ { "config": { "abort": { - "already_configured": "El lloc del controlador ja est\u00e0 configurat" + "already_configured": "El lloc del controlador ja est\u00e0 configurat", + "configuration_updated": "S'ha actualitzat la configuraci\u00f3.", + "reauth_successful": "Re-autenticaci\u00f3 realitzada correctament" }, "error": { "faulty_credentials": "[%key::common::config_flow::error::invalid_auth%]", "service_unavailable": "[%key::common::config_flow::error::cannot_connect%]", "unknown_client_mac": "No hi ha cap client disponible en aquesta adre\u00e7a MAC" }, + "flow_title": "Xarxa UniFi {site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/cs.json b/homeassistant/components/unifi/translations/cs.json index 1247a97de9d..0281dfbb750 100644 --- a/homeassistant/components/unifi/translations/cs.json +++ b/homeassistant/components/unifi/translations/cs.json @@ -1,13 +1,15 @@ { "config": { "abort": { - "already_configured": "Ovlada\u010d je ji\u017e nastaven" + "already_configured": "Ovlada\u010d je ji\u017e nastaven", + "reauth_successful": "Op\u011btovn\u00e9 ov\u011b\u0159en\u00ed bylo \u00fasp\u011b\u0161n\u00e9" }, "error": { "faulty_credentials": "Neplatn\u00e9 ov\u011b\u0159en\u00ed", "service_unavailable": "Nepoda\u0159ilo se p\u0159ipojit", "unknown_client_mac": "Na t\u00e9to MAC adrese nen\u00ed dostupn\u00fd \u017e\u00e1dn\u00fd klient" }, + "flow_title": "UniFi s\u00ed\u0165 {site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/da.json b/homeassistant/components/unifi/translations/da.json index 15ec878f1ce..84dafd36e1a 100644 --- a/homeassistant/components/unifi/translations/da.json +++ b/homeassistant/components/unifi/translations/da.json @@ -7,6 +7,7 @@ "faulty_credentials": "Ugyldige legitimationsoplysninger", "service_unavailable": "Service utilg\u00e6ngelig" }, + "flow_title": "UniFi-netv\u00e6rket {site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/de.json b/homeassistant/components/unifi/translations/de.json index 626236792ea..be38ddf1a4d 100644 --- a/homeassistant/components/unifi/translations/de.json +++ b/homeassistant/components/unifi/translations/de.json @@ -4,7 +4,7 @@ "already_configured": "Controller-Site ist bereits konfiguriert" }, "error": { - "faulty_credentials": "Ung\u00fcltige Anmeldeinformationen", + "faulty_credentials": "Ung\u00fcltige Authentifizierung", "service_unavailable": "Verbindung fehlgeschlagen", "unknown_client_mac": "Unter dieser MAC-Adresse ist kein Client verf\u00fcgbar." }, @@ -16,7 +16,7 @@ "port": "Port", "site": "Site-ID", "username": "Benutzername", - "verify_ssl": "Controller mit ordnungsgem\u00e4ssem Zertifikat" + "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" }, "title": "UniFi-Controller einrichten" } @@ -51,7 +51,9 @@ }, "simple_options": { "data": { - "track_clients": "Netzwerk Ger\u00e4te \u00fcberwachen" + "block_client": "Clients mit Netzwerkzugriffskontrolle", + "track_clients": "Netzwerger\u00e4te \u00fcberwachen", + "track_devices": "Verfolgen von Netzwerkger\u00e4ten (Ubiquiti-Ger\u00e4te)" }, "description": "Konfigurieren Sie die UniFi-Integration" }, diff --git a/homeassistant/components/unifi/translations/en.json b/homeassistant/components/unifi/translations/en.json index 968d90e377c..41507faa430 100644 --- a/homeassistant/components/unifi/translations/en.json +++ b/homeassistant/components/unifi/translations/en.json @@ -1,13 +1,16 @@ { "config": { "abort": { - "already_configured": "Controller site is already configured" + "already_configured": "Controller site is already configured", + "configuration_updated": "Configuration updated.", + "reauth_successful": "Re-authentication was successful" }, "error": { "faulty_credentials": "Invalid authentication", "service_unavailable": "Failed to connect", "unknown_client_mac": "No client available on that MAC address" }, + "flow_title": "UniFi Network {site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/es.json b/homeassistant/components/unifi/translations/es.json index 0fa4aaf2eb7..a676d70e88c 100644 --- a/homeassistant/components/unifi/translations/es.json +++ b/homeassistant/components/unifi/translations/es.json @@ -1,13 +1,15 @@ { "config": { "abort": { - "already_configured": "El sitio del controlador ya est\u00e1 configurado" + "already_configured": "El sitio del controlador ya est\u00e1 configurado", + "reauth_successful": "La reautenticaci\u00f3n se realiz\u00f3 correctamente" }, "error": { "faulty_credentials": "Autenticaci\u00f3n no v\u00e1lida", "service_unavailable": "Error al conectar", "unknown_client_mac": "Ning\u00fan cliente disponible en esa direcci\u00f3n MAC" }, + "flow_title": "Red UniFi {site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/et.json b/homeassistant/components/unifi/translations/et.json index 8e95da9aa5b..e9d76520435 100644 --- a/homeassistant/components/unifi/translations/et.json +++ b/homeassistant/components/unifi/translations/et.json @@ -1,13 +1,16 @@ { "config": { "abort": { - "already_configured": "Kontroller on juba seadistatud" + "already_configured": "Kontroller on juba seadistatud", + "configuration_updated": "Seaded on v\u00e4rskendatud.", + "reauth_successful": "Taastuvastamine \u00f5nnestus" }, "error": { "faulty_credentials": "Tuvastamine nurjus", "service_unavailable": "\u00dchendamine nurjus", "unknown_client_mac": "Sellel MAC-aadressil pole \u00fchtegi klienti saadaval" }, + "flow_title": "UniFi Network {site} ( {host} )", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/it.json b/homeassistant/components/unifi/translations/it.json index 79a7206923e..d50018227c5 100644 --- a/homeassistant/components/unifi/translations/it.json +++ b/homeassistant/components/unifi/translations/it.json @@ -1,13 +1,16 @@ { "config": { "abort": { - "already_configured": "Il sito del Controller \u00e8 gi\u00e0 configurato" + "already_configured": "Il sito del Controller \u00e8 gi\u00e0 configurato", + "configuration_updated": "Configurazione aggiornata.", + "reauth_successful": "La riautenticazione ha avuto successo" }, "error": { "faulty_credentials": "Autenticazione non valida", "service_unavailable": "Impossibile connettersi", "unknown_client_mac": "Nessun client disponibile su quell'indirizzo MAC" }, + "flow_title": "Rete UniFi {site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/no.json b/homeassistant/components/unifi/translations/no.json index 5cda9ad7ab5..72944a9d540 100644 --- a/homeassistant/components/unifi/translations/no.json +++ b/homeassistant/components/unifi/translations/no.json @@ -1,13 +1,16 @@ { "config": { "abort": { - "already_configured": "Kontroller nettstedet er allerede konfigurert" + "already_configured": "Kontroller nettstedet er allerede konfigurert", + "configuration_updated": "Konfigurasjonen er oppdatert.", + "reauth_successful": "Godkjenning p\u00e5 nytt var vellykket" }, "error": { "faulty_credentials": "Ugyldig godkjenning", "service_unavailable": "Tilkobling mislyktes", "unknown_client_mac": "Ingen klient tilgjengelig p\u00e5 den MAC-adressen" }, + "flow_title": "UniFi-nettverk {site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/pl.json b/homeassistant/components/unifi/translations/pl.json index 8ff5f1e4793..6c8c74e726a 100644 --- a/homeassistant/components/unifi/translations/pl.json +++ b/homeassistant/components/unifi/translations/pl.json @@ -1,13 +1,16 @@ { "config": { "abort": { - "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana" + "already_configured": "Us\u0142uga jest ju\u017c skonfigurowana", + "configuration_updated": "Konfiguracja zaktualizowana", + "reauth_successful": "Ponowne uwierzytelnienie powiod\u0142o si\u0119" }, "error": { "faulty_credentials": "Niepoprawne uwierzytelnienie", "service_unavailable": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", "unknown_client_mac": "Brak klienta z tym adresem MAC" }, + "flow_title": "Sie\u0107 UniFi {site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/ru.json b/homeassistant/components/unifi/translations/ru.json index 789212dca17..3b69bf0ee33 100644 --- a/homeassistant/components/unifi/translations/ru.json +++ b/homeassistant/components/unifi/translations/ru.json @@ -1,13 +1,15 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "reauth_successful": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430 \u0443\u0441\u043f\u0435\u0448\u043d\u043e." }, "error": { "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", "service_unavailable": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", "unknown_client_mac": "\u041d\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043d\u0430 \u044d\u0442\u043e\u043c MAC-\u0430\u0434\u0440\u0435\u0441\u0435." }, + "flow_title": "UniFi Network {site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/translations/tr.json b/homeassistant/components/unifi/translations/tr.json index 903a7aaa21f..c39fa08217a 100644 --- a/homeassistant/components/unifi/translations/tr.json +++ b/homeassistant/components/unifi/translations/tr.json @@ -1,9 +1,22 @@ { "config": { + "abort": { + "already_configured": "Denetleyici sitesi zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "configuration_updated": "Yap\u0131land\u0131rma g\u00fcncellendi.", + "reauth_successful": "Yeniden kimlik do\u011frulama ba\u015far\u0131l\u0131 oldu" + }, + "error": { + "faulty_credentials": "Ge\u00e7ersiz kimlik do\u011frulama", + "service_unavailable": "Ba\u011flanma hatas\u0131", + "unknown_client_mac": "Bu MAC adresinde kullan\u0131labilir istemci yok" + }, + "flow_title": "UniFi A\u011f\u0131 {site} ( {host} )", "step": { "user": { "data": { + "host": "Ana Bilgisayar", "password": "Parola", + "port": "Port", "username": "Kullan\u0131c\u0131 ad\u0131" } } diff --git a/homeassistant/components/unifi/translations/uk.json b/homeassistant/components/unifi/translations/uk.json new file mode 100644 index 00000000000..0f83c35840a --- /dev/null +++ b/homeassistant/components/unifi/translations/uk.json @@ -0,0 +1,66 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u0435." + }, + "error": { + "faulty_credentials": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "service_unavailable": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "unknown_client_mac": "\u041d\u0435\u043c\u0430\u0454 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0438\u0445 \u043a\u043b\u0456\u0454\u043d\u0442\u0456\u0432 \u043d\u0430 \u0446\u0456\u0439 MAC-\u0430\u0434\u0440\u0435\u0441\u0456." + }, + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "site": "ID \u0441\u0430\u0439\u0442\u0443", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", + "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL" + }, + "title": "UniFi Controller" + } + } + }, + "options": { + "step": { + "client_control": { + "data": { + "block_client": "\u041a\u043b\u0456\u0454\u043d\u0442\u0438 \u0437 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u043c \u043c\u0435\u0440\u0435\u0436\u0435\u0432\u043e\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0443", + "dpi_restrictions": "\u0414\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0438 \u0443\u043f\u0440\u0430\u0432\u043b\u0456\u043d\u043d\u044f \u0433\u0440\u0443\u043f\u0430\u043c\u0438 \u043e\u0431\u043c\u0435\u0436\u0435\u043d\u044c DPI", + "poe_clients": "\u0414\u043e\u0437\u0432\u043e\u043b\u0438\u0442\u0438 POE \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044c \u043a\u043b\u0456\u0454\u043d\u0442\u0456\u0432" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0435\u043b\u0435\u043c\u0435\u043d\u0442\u0456\u0432 \u0443\u043f\u0440\u0430\u0432\u043b\u0456\u043d\u043d\u044f. \n\n\u0421\u0442\u0432\u043e\u0440\u0456\u0442\u044c \u043f\u0435\u0440\u0435\u043c\u0438\u043a\u0430\u0447\u0456 \u0434\u043b\u044f \u0441\u0435\u0440\u0456\u0439\u043d\u0438\u0445 \u043d\u043e\u043c\u0435\u0440\u0456\u0432, \u0434\u043b\u044f \u044f\u043a\u0438\u0445 \u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044e\u0432\u0430\u0442\u0438 \u0434\u043e\u0441\u0442\u0443\u043f \u0434\u043e \u043c\u0435\u0440\u0435\u0436\u0456.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f UniFi. \u041a\u0440\u043e\u043a 2." + }, + "device_tracker": { + "data": { + "detection_time": "\u0427\u0430\u0441 \u0432\u0456\u0434 \u043e\u0441\u0442\u0430\u043d\u043d\u044c\u043e\u0433\u043e \u0441\u0435\u0430\u043d\u0441\u0443 \u0437\u0432'\u044f\u0437\u043a\u0443 \u0437 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c (\u0441\u0435\u043a.), \u043f\u043e \u0437\u0430\u043a\u0456\u043d\u0447\u0435\u043d\u043d\u044e \u044f\u043a\u043e\u0433\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043e\u0442\u0440\u0438\u043c\u0430\u0454 \u0441\u0442\u0430\u0442\u0443\u0441 \"\u041d\u0435 \u0432\u0434\u043e\u043c\u0430\".", + "ignore_wired_bug": "\u0412\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u043b\u043e\u0433\u0456\u043a\u0443 \u043f\u043e\u043c\u0438\u043b\u043a\u0438 \u0434\u043b\u044f \u0434\u0440\u043e\u0442\u043e\u0432\u0438\u0445 \u043a\u043b\u0456\u0454\u043d\u0442\u0456\u0432 UniFi", + "ssid_filter": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c SSID \u0434\u043b\u044f \u0432\u0456\u0434\u0441\u0442\u0435\u0436\u0435\u043d\u043d\u044f \u0431\u0435\u0437\u0434\u0440\u043e\u0442\u043e\u0432\u0438\u0445 \u043a\u043b\u0456\u0454\u043d\u0442\u0456\u0432", + "track_clients": "\u0412\u0456\u0434\u0441\u0442\u0435\u0436\u0435\u043d\u043d\u044f \u043a\u043b\u0456\u0454\u043d\u0442\u0456\u0432 \u043c\u0435\u0440\u0435\u0436\u0456", + "track_devices": "\u0412\u0456\u0434\u0441\u0442\u0435\u0436\u0435\u043d\u043d\u044f \u043c\u0435\u0440\u0435\u0436\u0435\u0432\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 (\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 Ubiquiti)", + "track_wired_clients": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u043f\u0440\u043e\u0432\u0456\u0434\u043d\u0438\u0445 \u043c\u0435\u0440\u0435\u0436\u043d\u0438\u0445 \u043a\u043b\u0456\u0454\u043d\u0442\u0456\u0432" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0456\u0434\u0441\u0442\u0435\u0436\u0435\u043d\u043d\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f UniFi. \u041a\u0440\u043e\u043a 1" + }, + "simple_options": { + "data": { + "block_client": "\u041a\u043b\u0456\u0454\u043d\u0442\u0438 \u0437 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u043c \u043c\u0435\u0440\u0435\u0436\u0435\u0432\u043e\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0443", + "track_clients": "\u0412\u0456\u0434\u0441\u0442\u0435\u0436\u0435\u043d\u043d\u044f \u043a\u043b\u0456\u0454\u043d\u0442\u0456\u0432 \u043c\u0435\u0440\u0435\u0436\u0456", + "track_devices": "\u0412\u0456\u0434\u0441\u0442\u0435\u0436\u0435\u043d\u043d\u044f \u043c\u0435\u0440\u0435\u0436\u0435\u0432\u0438\u0445 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 (\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 Ubiquiti)" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 UniFi." + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "\u0414\u0430\u0442\u0447\u0438\u043a\u0438 \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u043d\u043e\u0457 \u0437\u0434\u0430\u0442\u043d\u043e\u0441\u0442\u0456 \u0434\u043b\u044f \u043c\u0435\u0440\u0435\u0436\u0435\u0432\u0438\u0445 \u043a\u043b\u0456\u0454\u043d\u0442\u0456\u0432", + "allow_uptime_sensors": "\u0421\u0435\u043d\u0441\u043e\u0440\u0438 \u0447\u0430\u0441\u0443 \u0440\u043e\u0431\u043e\u0442\u0438 \u0434\u043b\u044f \u043c\u0435\u0440\u0435\u0436\u0435\u0432\u0438\u0445 \u043a\u043b\u0456\u0454\u043d\u0442\u0456\u0432" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0441\u0435\u043d\u0441\u043e\u0440\u0456\u0432 \u0441\u0442\u0430\u0442\u0438\u0441\u0442\u0438\u043a\u0438", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f UniFi. \u043a\u0440\u043e\u043a 3" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/zh-Hant.json b/homeassistant/components/unifi/translations/zh-Hant.json index d87f8cf51e0..add0a387309 100644 --- a/homeassistant/components/unifi/translations/zh-Hant.json +++ b/homeassistant/components/unifi/translations/zh-Hant.json @@ -1,13 +1,16 @@ { "config": { "abort": { - "already_configured": "\u63a7\u5236\u5668\u4f4d\u5740\u5df2\u7d93\u8a2d\u5b9a" + "already_configured": "\u63a7\u5236\u5668\u4f4d\u5740\u5df2\u7d93\u8a2d\u5b9a", + "configuration_updated": "\u8a2d\u5b9a\u5df2\u66f4\u65b0\u3002", + "reauth_successful": "\u91cd\u65b0\u8a8d\u8b49\u6210\u529f" }, "error": { "faulty_credentials": "\u9a57\u8b49\u78bc\u7121\u6548", "service_unavailable": "\u9023\u7dda\u5931\u6557", "unknown_client_mac": "\u8a72 Mac \u4f4d\u5740\u7121\u53ef\u7528\u5ba2\u6236\u7aef" }, + "flow_title": "UniFi Network {site} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/unifi/unifi_client.py b/homeassistant/components/unifi/unifi_client.py index 17f5a473211..9710f3ace29 100644 --- a/homeassistant/components/unifi/unifi_client.py +++ b/homeassistant/components/unifi/unifi_client.py @@ -12,11 +12,7 @@ class UniFiClient(UniFiBase): super().__init__(client, controller) self._is_wired = client.mac not in controller.wireless_clients - - @property - def client(self): - """Wrap item.""" - return self._item + self.client = self._item @property def is_wired(self): @@ -29,6 +25,7 @@ class UniFiClient(UniFiBase): if self.controller.option_ignore_wired_bug: return self.client.is_wired + return self._is_wired @property diff --git a/homeassistant/components/unifi/unifi_entity_base.py b/homeassistant/components/unifi/unifi_entity_base.py index 7b45d309c14..904348f6324 100644 --- a/homeassistant/components/unifi/unifi_entity_base.py +++ b/homeassistant/components/unifi/unifi_entity_base.py @@ -39,7 +39,7 @@ class UniFiBase(Entity): self.key, ) for signal, method in ( - (self.controller.signal_reachable, self.async_update_callback), + (self.controller.signal_reachable, self.async_signal_reachable_callback), (self.controller.signal_options_update, self.options_updated), (self.controller.signal_remove, self.remove_item), ): @@ -57,6 +57,11 @@ class UniFiBase(Entity): self._item.remove_callback(self.async_update_callback) self.controller.entities[self.DOMAIN][self.TYPE].remove(self.key) + @callback + def async_signal_reachable_callback(self) -> None: + """Call when controller connection state change.""" + self.async_update_callback() + @callback def async_update_callback(self) -> None: """Update the entity's state.""" diff --git a/homeassistant/components/upb/manifest.json b/homeassistant/components/upb/manifest.json index 1f170030df6..9ad43117225 100644 --- a/homeassistant/components/upb/manifest.json +++ b/homeassistant/components/upb/manifest.json @@ -2,7 +2,7 @@ "domain": "upb", "name": "Universal Powerline Bus (UPB)", "documentation": "https://www.home-assistant.io/integrations/upb", - "requirements": ["upb_lib==0.4.11"], + "requirements": ["upb_lib==0.4.12"], "codeowners": ["@gwww"], "config_flow": true } diff --git a/homeassistant/components/upb/translations/de.json b/homeassistant/components/upb/translations/de.json index ea6f1d37150..908db20f22b 100644 --- a/homeassistant/components/upb/translations/de.json +++ b/homeassistant/components/upb/translations/de.json @@ -1,9 +1,12 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Fehler beim Herstellen einer Verbindung zu UPB PIM. Versuchen Sie es erneut.", - "invalid_upb_file": "Fehlende oder ung\u00fcltige UPB UPStart-Exportdatei, \u00fcberpr\u00fcfen Sie den Namen und den Pfad der Datei.", - "unknown": "Unerwarteter Fehler." + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_upb_file": "Fehlende oder ung\u00fcltige UPB UPStart-Exportdatei, \u00fcberpr\u00fcfe den Namen und den Pfad der Datei.", + "unknown": "Unerwarteter Fehler" }, "step": { "user": { @@ -12,7 +15,7 @@ "file_path": "Pfad und Name der UPStart UPB-Exportdatei.", "protocol": "Protokoll" }, - "title": "Stellen Sie eine Verbindung zu UPB PIM her" + "title": "Stelle eine Verbindung zu UPB PIM her" } } } diff --git a/homeassistant/components/upb/translations/tr.json b/homeassistant/components/upb/translations/tr.json new file mode 100644 index 00000000000..818531fcaa0 --- /dev/null +++ b/homeassistant/components/upb/translations/tr.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upb/translations/uk.json b/homeassistant/components/upb/translations/uk.json new file mode 100644 index 00000000000..062503848a8 --- /dev/null +++ b/homeassistant/components/upb/translations/uk.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_upb_file": "\u0412\u0456\u0434\u0441\u0443\u0442\u043d\u0456\u0439 \u0430\u0431\u043e \u043d\u0435\u0434\u0456\u0439\u0441\u043d\u0438\u0439 \u0444\u0430\u0439\u043b \u0435\u043a\u0441\u043f\u043e\u0440\u0442\u0443 UPB UPStart, \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0456\u043c'\u044f \u0456 \u0448\u043b\u044f\u0445 \u0434\u043e \u0444\u0430\u0439\u043b\u0443.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "address": "\u0410\u0434\u0440\u0435\u0441\u0430 (\u0434\u0438\u0432. \u043e\u043f\u0438\u0441 \u0432\u0438\u0449\u0435)", + "file_path": "\u0428\u043b\u044f\u0445 \u0456 \u0456\u043c'\u044f \u0444\u0430\u0439\u043b\u0443 \u0435\u043a\u0441\u043f\u043e\u0440\u0442\u0443 UPStart UPB.", + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b" + }, + "description": "\u0420\u044f\u0434\u043e\u043a \u0430\u0434\u0440\u0435\u0441\u0438 \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 'address[:port]' \u0434\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 'tcp' (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: '192.168.1.42'). \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 'port' \u0432\u043a\u0430\u0437\u0443\u0432\u0430\u0442\u0438 \u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e, \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0432\u0456\u043d \u0434\u043e\u0440\u0456\u0432\u043d\u044e\u0454 2101. \u0414\u043b\u044f \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 'serial' \u0430\u0434\u0440\u0435\u0441\u0430 \u043f\u043e\u0432\u0438\u043d\u043d\u0430 \u0431\u0443\u0442\u0438 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 'tty[:baud]' (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: '/dev/ttyS1'). \u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440 'baud' \u0432\u043a\u0430\u0437\u0443\u0432\u0430\u0442\u0438 \u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e, \u0437\u0430 \u0437\u0430\u043c\u043e\u0432\u0447\u0443\u0432\u0430\u043d\u043d\u044f\u043c \u0432\u0456\u043d \u0434\u043e\u0440\u0456\u0432\u043d\u044e\u0454 4800.", + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e UPB PIM" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upcloud/translations/de.json b/homeassistant/components/upcloud/translations/de.json index 76bbc705690..ee1802f1d38 100644 --- a/homeassistant/components/upcloud/translations/de.json +++ b/homeassistant/components/upcloud/translations/de.json @@ -1,7 +1,8 @@ { "config": { "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" }, "step": { "user": { diff --git a/homeassistant/components/upcloud/translations/tr.json b/homeassistant/components/upcloud/translations/tr.json new file mode 100644 index 00000000000..f1840698493 --- /dev/null +++ b/homeassistant/components/upcloud/translations/tr.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upcloud/translations/uk.json b/homeassistant/components/upcloud/translations/uk.json new file mode 100644 index 00000000000..bf8781c1eb2 --- /dev/null +++ b/homeassistant/components/upcloud/translations/uk.json @@ -0,0 +1,25 @@ +{ + "config": { + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445, \u043c\u0456\u043d\u0456\u043c\u0443\u043c 30)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index 3d7b8b626c9..13497da8290 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -93,7 +93,11 @@ async def async_setup(hass, config): "You are on the latest version (%s) of Home Assistant", newest ) elif StrictVersion(newest) < StrictVersion(current_version): - _LOGGER.debug("Local version is newer than the latest version (%s)", newest) + _LOGGER.debug( + "Local version (%s) is newer than the latest available version (%s)", + current_version, + newest, + ) _LOGGER.debug("Update available: %s", update_available) diff --git a/homeassistant/components/upnp/translations/ro.json b/homeassistant/components/upnp/translations/ro.json index ceb1c19131a..2fd83a0b371 100644 --- a/homeassistant/components/upnp/translations/ro.json +++ b/homeassistant/components/upnp/translations/ro.json @@ -7,6 +7,13 @@ "few": "", "one": "Unul", "other": "" + }, + "step": { + "init": { + "few": "Pu\u021bine", + "one": "Unul", + "other": "Altele" + } } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/tr.json b/homeassistant/components/upnp/translations/tr.json new file mode 100644 index 00000000000..2715f66e090 --- /dev/null +++ b/homeassistant/components/upnp/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "flow_title": "UPnP / IGD: {name}", + "step": { + "ssdp_confirm": { + "description": "Bu UPnP / IGD cihaz\u0131n\u0131 kurmak istiyor musunuz?" + }, + "user": { + "data": { + "scan_interval": "G\u00fcncelleme aral\u0131\u011f\u0131 (saniye, minimum 30)", + "usn": "Cihaz" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/uk.json b/homeassistant/components/upnp/translations/uk.json index 0b8747f902e..905958eeca9 100644 --- a/homeassistant/components/upnp/translations/uk.json +++ b/homeassistant/components/upnp/translations/uk.json @@ -1,7 +1,21 @@ { "config": { "abort": { - "already_configured": "UPnP/IGD \u0432\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0454\u043d\u043e" + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "incomplete_discovery": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043f\u0440\u043e\u0446\u0435\u0441.", + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456." + }, + "flow_title": "UPnP/IGD: {name}", + "step": { + "ssdp_confirm": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u0446\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 UPnP / IGD?" + }, + "user": { + "data": { + "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445, \u043c\u0456\u043d\u0456\u043c\u0443\u043c 30)", + "usn": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py index da748c20c1c..4f5cfa3907e 100644 --- a/homeassistant/components/uvc/camera.py +++ b/homeassistant/components/uvc/camera.py @@ -1,4 +1,5 @@ """Support for Ubiquiti's UVC cameras.""" +from datetime import datetime import logging import re @@ -109,16 +110,39 @@ class UnifiVideoCamera(Camera): return 0 + @property + def state_attributes(self): + """Return the camera state attributes.""" + attr = super().state_attributes + if self.motion_detection_enabled: + attr["last_recording_start_time"] = timestamp_ms_to_date( + self._caminfo["lastRecordingStartTime"] + ) + return attr + @property def is_recording(self): """Return true if the camera is recording.""" - return self._caminfo["recordingSettings"]["fullTimeRecordEnabled"] + recording_state = "DISABLED" + if "recordingIndicator" in self._caminfo.keys(): + recording_state = self._caminfo["recordingIndicator"] + + return ( + self._caminfo["recordingSettings"]["fullTimeRecordEnabled"] + or recording_state == "MOTION_INPROGRESS" + or recording_state == "MOTION_FINISHED" + ) @property def motion_detection_enabled(self): """Camera Motion Detection Status.""" return self._caminfo["recordingSettings"]["motionRecordEnabled"] + @property + def unique_id(self) -> str: + """Return a unique identifier for this client.""" + return self._uuid + @property def brand(self): """Return the brand of this camera.""" @@ -230,3 +254,9 @@ class UnifiVideoCamera(Camera): def update(self): """Update the info.""" self._caminfo = self._nvr.get_camera(self._uuid) + + +def timestamp_ms_to_date(epoch_ms) -> datetime or None: + """Convert millisecond timestamp to datetime.""" + if epoch_ms: + return datetime.fromtimestamp(epoch_ms / 1000) diff --git a/homeassistant/components/vacuum/translations/de.json b/homeassistant/components/vacuum/translations/de.json index be137a5566b..8de386b3506 100644 --- a/homeassistant/components/vacuum/translations/de.json +++ b/homeassistant/components/vacuum/translations/de.json @@ -18,7 +18,7 @@ "cleaning": "Reinigen", "docked": "Angedockt", "error": "Fehler", - "idle": "Standby", + "idle": "Unt\u00e4tig", "off": "Aus", "on": "An", "paused": "Pausiert", diff --git a/homeassistant/components/vacuum/translations/uk.json b/homeassistant/components/vacuum/translations/uk.json index 9febc8aff1f..64223a85f74 100644 --- a/homeassistant/components/vacuum/translations/uk.json +++ b/homeassistant/components/vacuum/translations/uk.json @@ -1,4 +1,18 @@ { + "device_automation": { + "action_type": { + "clean": "\u0412\u0456\u0434\u043f\u0440\u0430\u0432\u0438\u0442\u0438 {entity_name} \u0440\u043e\u0431\u0438\u0442\u0438 \u043f\u0440\u0438\u0431\u0438\u0440\u0430\u043d\u043d\u044f", + "dock": "{entity_name}: \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u0442\u0438 \u043d\u0430 \u0434\u043e\u043a-\u0441\u0442\u0430\u043d\u0446\u0456\u044e" + }, + "condition_type": { + "is_cleaning": "{entity_name} \u0432\u0438\u043a\u043e\u043d\u0443\u0454 \u043f\u0440\u0438\u0431\u0438\u0440\u0430\u043d\u043d\u044f", + "is_docked": "{entity_name} \u043d\u0430 \u0434\u043e\u043a-\u0441\u0442\u0430\u043d\u0446\u0456\u0457" + }, + "trigger_type": { + "cleaning": "{entity_name} \u043f\u043e\u0447\u0438\u043d\u0430\u0454 \u043f\u0440\u0438\u0431\u0438\u0440\u0430\u043d\u043d\u044f", + "docked": "{entity_name} \u0441\u0442\u0438\u043a\u0443\u0454\u0442\u044c\u0441\u044f \u0437 \u0434\u043e\u043a-\u0441\u0442\u0430\u043d\u0446\u0456\u0454\u044e" + } + }, "state": { "_": { "cleaning": "\u041f\u0440\u0438\u0431\u0438\u0440\u0430\u043d\u043d\u044f", @@ -8,7 +22,7 @@ "off": "\u0412\u0438\u043c\u043a\u043d\u0435\u043d\u043e", "on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e", "paused": "\u041f\u0440\u0438\u0437\u0443\u043f\u0438\u043d\u0435\u043d\u043e", - "returning": "\u041f\u043e\u0432\u0435\u0440\u043d\u0435\u043d\u043d\u044f \u0434\u043e \u0434\u043e\u043a\u0430" + "returning": "\u041f\u043e\u0432\u0435\u0440\u043d\u0435\u043d\u043d\u044f \u043d\u0430 \u0434\u043e\u043a-\u0441\u0442\u0430\u043d\u0446\u0456\u044e" } }, "title": "\u041f\u0438\u043b\u043e\u0441\u043e\u0441" diff --git a/homeassistant/components/velbus/manifest.json b/homeassistant/components/velbus/manifest.json index b804c34c792..2e1612554b5 100644 --- a/homeassistant/components/velbus/manifest.json +++ b/homeassistant/components/velbus/manifest.json @@ -2,7 +2,7 @@ "domain": "velbus", "name": "Velbus", "documentation": "https://www.home-assistant.io/integrations/velbus", - "requirements": ["python-velbus==2.1.1"], + "requirements": ["python-velbus==2.1.2"], "config_flow": true, "codeowners": ["@Cereal2nd", "@brefra"] } diff --git a/homeassistant/components/velbus/translations/de.json b/homeassistant/components/velbus/translations/de.json index c6c872c85e6..9bbb23b1bcd 100644 --- a/homeassistant/components/velbus/translations/de.json +++ b/homeassistant/components/velbus/translations/de.json @@ -1,7 +1,11 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler" + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "user": { diff --git a/homeassistant/components/velbus/translations/tr.json b/homeassistant/components/velbus/translations/tr.json new file mode 100644 index 00000000000..e7ee4ea7157 --- /dev/null +++ b/homeassistant/components/velbus/translations/tr.json @@ -0,0 +1,11 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/velbus/translations/uk.json b/homeassistant/components/velbus/translations/uk.json new file mode 100644 index 00000000000..6e8b97cc457 --- /dev/null +++ b/homeassistant/components/velbus/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430", + "port": "\u0420\u044f\u0434\u043e\u043a \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f" + }, + "title": "Velbus" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/velux/cover.py b/homeassistant/components/velux/cover.py index e8e210c1e53..187c0d36178 100644 --- a/homeassistant/components/velux/cover.py +++ b/homeassistant/components/velux/cover.py @@ -4,6 +4,7 @@ from pyvlx.opening_device import Awning, Blind, GarageDoor, Gate, RollerShutter, from homeassistant.components.cover import ( ATTR_POSITION, + ATTR_TILT_POSITION, DEVICE_CLASS_AWNING, DEVICE_CLASS_BLIND, DEVICE_CLASS_GARAGE, @@ -11,9 +12,13 @@ from homeassistant.components.cover import ( DEVICE_CLASS_SHUTTER, DEVICE_CLASS_WINDOW, SUPPORT_CLOSE, + SUPPORT_CLOSE_TILT, SUPPORT_OPEN, + SUPPORT_OPEN_TILT, SUPPORT_SET_POSITION, + SUPPORT_SET_TILT_POSITION, SUPPORT_STOP, + SUPPORT_STOP_TILT, CoverEntity, ) from homeassistant.core import callback @@ -69,13 +74,30 @@ class VeluxCover(CoverEntity): @property def supported_features(self): """Flag supported features.""" - return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION | SUPPORT_STOP + supported_features = ( + SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION | SUPPORT_STOP + ) + if self.current_cover_tilt_position is not None: + supported_features |= ( + SUPPORT_OPEN_TILT + | SUPPORT_CLOSE_TILT + | SUPPORT_SET_TILT_POSITION + | SUPPORT_STOP_TILT + ) + return supported_features @property def current_cover_position(self): """Return the current position of the cover.""" return 100 - self.node.position.position_percent + @property + def current_cover_tilt_position(self): + """Return the current position of the cover.""" + if isinstance(self.node, Blind): + return 100 - self.node.orientation.position_percent + return None + @property def device_class(self): """Define this cover as either awning, blind, garage, gate, shutter or window.""" @@ -118,3 +140,23 @@ class VeluxCover(CoverEntity): async def async_stop_cover(self, **kwargs): """Stop the cover.""" await self.node.stop(wait_for_completion=False) + + async def async_close_cover_tilt(self, **kwargs): + """Close cover tilt.""" + await self.node.close_orientation(wait_for_completion=False) + + async def async_open_cover_tilt(self, **kwargs): + """Open cover tilt.""" + await self.node.open_orientation(wait_for_completion=False) + + async def async_stop_cover_tilt(self, **kwargs): + """Stop cover tilt.""" + await self.node.stop_orientation(wait_for_completion=False) + + async def async_set_cover_tilt_position(self, **kwargs): + """Move cover tilt to a specific position.""" + position_percent = 100 - kwargs[ATTR_TILT_POSITION] + orientation = Position(position_percent=position_percent) + await self.node.set_orientation( + orientation=orientation, wait_for_completion=False + ) diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index fdc8503ed70..3fd1c189b63 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -110,9 +110,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b # Initialize the Vera controller. subscription_registry = SubscriptionRegistry(hass) controller = veraApi.VeraController(base_url, subscription_registry) - await hass.async_add_executor_job(controller.start) - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, controller.stop) try: all_devices = await hass.async_add_executor_job(controller.get_devices) @@ -151,6 +148,13 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.config_entries.async_forward_entry_setup(config_entry, platform) ) + def stop_subscription(event): + """Stop SubscriptionRegistry updates.""" + controller.stop() + + await hass.async_add_executor_job(controller.start) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription) + return True diff --git a/homeassistant/components/vera/translations/tr.json b/homeassistant/components/vera/translations/tr.json new file mode 100644 index 00000000000..35e81599bb1 --- /dev/null +++ b/homeassistant/components/vera/translations/tr.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "cannot_connect": "{base_url} url'si ile denetleyiciye ba\u011flan\u0131lamad\u0131" + } + }, + "options": { + "step": { + "init": { + "title": "Vera denetleyici se\u00e7enekleri" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vera/translations/uk.json b/homeassistant/components/vera/translations/uk.json new file mode 100644 index 00000000000..8c591a1cc10 --- /dev/null +++ b/homeassistant/components/vera/translations/uk.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "cannot_connect": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0456\u0437 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u043e\u043c \u0437\u0430 \u0430\u0434\u0440\u0435\u0441\u043e\u044e {base_url}." + }, + "step": { + "user": { + "data": { + "exclude": "ID \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 Vera, \u0434\u043b\u044f \u0432\u0438\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0437 Home Assistant", + "lights": "ID \u0432\u0438\u043c\u0438\u043a\u0430\u0447\u0456\u0432 Vera, \u0434\u043b\u044f \u0456\u043c\u043f\u043e\u0440\u0442\u0443 \u0432 \u043e\u0441\u0432\u0456\u0442\u043b\u0435\u043d\u043d\u044f", + "vera_controller_url": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430" + }, + "description": "\u0412\u043a\u0430\u0436\u0456\u0442\u044c URL-\u0430\u0434\u0440\u0435\u0441\u0443 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430 \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0456 'address[:port]' (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: 'http://192.168.1.161:3480').", + "title": "Vera" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "exclude": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 Vera \u0434\u043b\u044f \u0432\u0438\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0437 Home Assistant.", + "lights": "\u0406\u0434\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u043e\u0440\u0438 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 Vera \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u043f\u0440\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f \u0437 \u0432\u0438\u043c\u0438\u043a\u0430\u0447\u0430 \u0432 \u043e\u0441\u0432\u0456\u0442\u043b\u0435\u043d\u043d\u044f \u0432 Home Assistant." + }, + "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438 \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0434\u043e\u0434\u0430\u0442\u043a\u043e\u0432\u0456 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438: https://www.home-assistant.io/integrations/vera/.\n\u0414\u043b\u044f \u0432\u043d\u0435\u0441\u0435\u043d\u043d\u044f \u0431\u0443\u0434\u044c-\u044f\u043a\u0438\u0445 \u0437\u043c\u0456\u043d \u043f\u043e\u0442\u0440\u0456\u0431\u0435\u043d \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Home Assistant. \u0429\u043e\u0431 \u043e\u0447\u0438\u0441\u0442\u0438\u0442\u0438 \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f, \u043f\u043e\u0441\u0442\u0430\u0432\u0442\u0435 \u043f\u0440\u043e\u0431\u0456\u043b.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u0440\u0430 Vera" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/verisure/manifest.json b/homeassistant/components/verisure/manifest.json index 6260f4a9ffc..22c5e0c2362 100644 --- a/homeassistant/components/verisure/manifest.json +++ b/homeassistant/components/verisure/manifest.json @@ -2,6 +2,6 @@ "domain": "verisure", "name": "Verisure", "documentation": "https://www.home-assistant.io/integrations/verisure", - "requirements": ["jsonpath==0.82", "vsure==1.5.4"], + "requirements": ["jsonpath==0.82", "vsure==1.6.1"], "codeowners": ["@frenck"] } diff --git a/homeassistant/components/vesync/translations/de.json b/homeassistant/components/vesync/translations/de.json index c52b10c3293..ea05a60ff82 100644 --- a/homeassistant/components/vesync/translations/de.json +++ b/homeassistant/components/vesync/translations/de.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, + "error": { + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, "step": { "user": { "data": { diff --git a/homeassistant/components/vesync/translations/tr.json b/homeassistant/components/vesync/translations/tr.json new file mode 100644 index 00000000000..8b4f8b60630 --- /dev/null +++ b/homeassistant/components/vesync/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "E-posta" + }, + "title": "Kullan\u0131c\u0131 Ad\u0131 ve \u015eifre Girin" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vesync/translations/uk.json b/homeassistant/components/vesync/translations/uk.json new file mode 100644 index 00000000000..7f6b3a46b15 --- /dev/null +++ b/homeassistant/components/vesync/translations/uk.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "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." + }, + "error": { + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438" + }, + "title": "VeSync" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index ddfb28478df..d1accd8ea0a 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -2,6 +2,7 @@ import logging import requests +import voluptuous as vol from homeassistant.components.climate import ClimateEntity from homeassistant.components.climate.const import ( @@ -16,6 +17,7 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, PRECISION_WHOLE, TEMP_CELSIUS +from homeassistant.helpers import entity_platform from . import ( DOMAIN as VICARE_DOMAIN, @@ -28,7 +30,11 @@ from . import ( _LOGGER = logging.getLogger(__name__) +SERVICE_SET_VICARE_MODE = "set_vicare_mode" +SERVICE_SET_VICARE_MODE_ATTR_MODE = "vicare_mode" + VICARE_MODE_DHW = "dhw" +VICARE_MODE_HEATING = "heating" VICARE_MODE_DHWANDHEATING = "dhwAndHeating" VICARE_MODE_DHWANDHEATINGCOOLING = "dhwAndHeatingCooling" VICARE_MODE_FORCEDREDUCED = "forcedReduced" @@ -55,6 +61,7 @@ SUPPORT_FLAGS_HEATING = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE VICARE_TO_HA_HVAC_HEATING = { VICARE_MODE_DHW: HVAC_MODE_OFF, + VICARE_MODE_HEATING: HVAC_MODE_HEAT, VICARE_MODE_DHWANDHEATING: HVAC_MODE_AUTO, VICARE_MODE_DHWANDHEATINGCOOLING: HVAC_MODE_AUTO, VICARE_MODE_FORCEDREDUCED: HVAC_MODE_OFF, @@ -79,22 +86,36 @@ HA_TO_VICARE_PRESET_HEATING = { } -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform( + hass, hass_config, async_add_entities, discovery_info=None +): """Create the ViCare climate devices.""" if discovery_info is None: return vicare_api = hass.data[VICARE_DOMAIN][VICARE_API] heating_type = hass.data[VICARE_DOMAIN][VICARE_HEATING_TYPE] - add_entities( + async_add_entities( [ ViCareClimate( - f"{hass.data[VICARE_DOMAIN][VICARE_NAME]} Heating", + f"{hass.data[VICARE_DOMAIN][VICARE_NAME]} Heating", vicare_api, heating_type, ) ] ) + platform = entity_platform.current_platform.get() + + platform.async_register_entity_service( + SERVICE_SET_VICARE_MODE, + { + vol.Required(SERVICE_SET_VICARE_MODE_ATTR_MODE): vol.In( + VICARE_TO_HA_HVAC_HEATING + ), + }, + "set_vicare_mode", + ) + class ViCareClimate(ClimateEntity): """Representation of the ViCare heating climate device.""" @@ -154,7 +175,6 @@ class ViCareClimate(ClimateEntity): elif self._heating_type == HeatingType.heatpump: self._current_action = self._api.getCompressorActive() - except requests.exceptions.ConnectionError: _LOGGER.error("Unable to retrieve data from ViCare server") except ValueError: @@ -194,10 +214,9 @@ class ViCareClimate(ClimateEntity): """Set a new hvac mode on the ViCare API.""" vicare_mode = HA_TO_VICARE_HVAC_HEATING.get(hvac_mode) if vicare_mode is None: - _LOGGER.error( - "Cannot set invalid vicare mode: %s / %s", hvac_mode, vicare_mode + raise ValueError( + f"Cannot set invalid vicare mode: {hvac_mode} / {vicare_mode}" ) - return _LOGGER.debug("Setting hvac mode to %s / %s", hvac_mode, vicare_mode) self._api.setMode(vicare_mode) @@ -250,12 +269,9 @@ class ViCareClimate(ClimateEntity): """Set new preset mode and deactivate any existing programs.""" vicare_program = HA_TO_VICARE_PRESET_HEATING.get(preset_mode) if vicare_program is None: - _LOGGER.error( - "Cannot set invalid vicare program: %s / %s", - preset_mode, - vicare_program, + raise ValueError( + f"Cannot set invalid vicare program: {preset_mode}/{vicare_program}" ) - return _LOGGER.debug("Setting preset to %s / %s", preset_mode, vicare_program) self._api.deactivateProgram(self._current_program) @@ -265,3 +281,10 @@ class ViCareClimate(ClimateEntity): def device_state_attributes(self): """Show Device Attributes.""" return self._attributes + + def set_vicare_mode(self, vicare_mode): + """Service function to set vicare modes directly.""" + if vicare_mode not in VICARE_TO_HA_HVAC_HEATING: + raise ValueError(f"Cannot set invalid vicare mode: {vicare_mode}") + + self._api.setMode(vicare_mode) diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index a91867b7a19..2eb40645e58 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -3,5 +3,5 @@ "name": "Viessmann ViCare", "documentation": "https://www.home-assistant.io/integrations/vicare", "codeowners": ["@oischinger"], - "requirements": ["PyViCare==0.2.0"] + "requirements": ["PyViCare==0.2.5"] } diff --git a/homeassistant/components/vicare/sensor.py b/homeassistant/components/vicare/sensor.py index d54fc6001cc..5e14795d540 100644 --- a/homeassistant/components/vicare/sensor.py +++ b/homeassistant/components/vicare/sensor.py @@ -8,11 +8,9 @@ from homeassistant.const import ( CONF_ICON, CONF_NAME, CONF_UNIT_OF_MEASUREMENT, - DEVICE_CLASS_POWER, DEVICE_CLASS_TEMPERATURE, ENERGY_KILO_WATT_HOUR, PERCENTAGE, - POWER_WATT, TEMP_CELSIUS, ) from homeassistant.helpers.entity import Entity @@ -155,13 +153,6 @@ SENSOR_TYPES = { CONF_GETTER: lambda api: api.getBurnerHours(), CONF_DEVICE_CLASS: None, }, - SENSOR_BURNER_POWER: { - CONF_NAME: "Burner Current Power", - CONF_ICON: None, - CONF_UNIT_OF_MEASUREMENT: POWER_WATT, - CONF_GETTER: lambda api: api.getCurrentPower(), - CONF_DEVICE_CLASS: DEVICE_CLASS_POWER, - }, # heatpump sensors SENSOR_COMPRESSOR_STARTS: { CONF_NAME: "Compressor Starts", @@ -194,7 +185,6 @@ SENSORS_BY_HEATINGTYPE = { SENSOR_BURNER_HOURS, SENSOR_BURNER_MODULATION, SENSOR_BURNER_STARTS, - SENSOR_BURNER_POWER, SENSOR_DHW_GAS_CONSUMPTION_TODAY, SENSOR_DHW_GAS_CONSUMPTION_THIS_WEEK, SENSOR_DHW_GAS_CONSUMPTION_THIS_MONTH, diff --git a/homeassistant/components/vicare/services.yaml b/homeassistant/components/vicare/services.yaml new file mode 100644 index 00000000000..2efaf530a9c --- /dev/null +++ b/homeassistant/components/vicare/services.yaml @@ -0,0 +1,9 @@ +set_vicare_mode: + description: Set a ViCare mode. + fields: + entity_id: + description: Name(s) of vicare climate entities. + example: "climate.vicare_heating" + vicare_mode: + description: ViCare mode. One of "dhw", "dhwAndHeating", "heating", "dhwAndHeatingCooling", "forcedReduced", "forcedNormal" or "standby" + example: "dhw" diff --git a/homeassistant/components/vilfo/translations/de.json b/homeassistant/components/vilfo/translations/de.json index 4880154b58e..8f20c074ff4 100644 --- a/homeassistant/components/vilfo/translations/de.json +++ b/homeassistant/components/vilfo/translations/de.json @@ -4,9 +4,9 @@ "already_configured": "Dieser Vilfo Router ist bereits konfiguriert." }, "error": { - "cannot_connect": "Verbindung nicht m\u00f6glich. Bitte \u00fcberpr\u00fcfen Sie die von Ihnen angegebenen Informationen und versuchen Sie es erneut.", - "invalid_auth": "Ung\u00fcltige Authentifizierung. Bitte \u00fcberpr\u00fcfen Sie den Zugriffstoken und versuchen Sie es erneut.", - "unknown": "Beim Einrichten der Integration ist ein unerwarteter Fehler aufgetreten." + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung. Bitte \u00fcberpr\u00fcfe den Zugriffstoken und versuche es erneut.", + "unknown": "Unerwarteter Fehler" }, "step": { "user": { diff --git a/homeassistant/components/vilfo/translations/tr.json b/homeassistant/components/vilfo/translations/tr.json new file mode 100644 index 00000000000..dc66041e35a --- /dev/null +++ b/homeassistant/components/vilfo/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "access_token": "Eri\u015fim Belirteci", + "host": "Ana Bilgisayar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/translations/uk.json b/homeassistant/components/vilfo/translations/uk.json new file mode 100644 index 00000000000..1a93176f290 --- /dev/null +++ b/homeassistant/components/vilfo/translations/uk.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443", + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0430 Vilfo. \u0412\u043a\u0430\u0436\u0456\u0442\u044c \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u0440\u043e\u0443\u0442\u0435\u0440\u0430 \u0456 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443 API. \u0414\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0446\u0456\u0454\u0457 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457, \u0432\u0456\u0434\u0432\u0456\u0434\u0430\u0439\u0442\u0435 \u0432\u0435\u0431-\u0441\u0430\u0439\u0442: https://www.home-assistant.io/integrations/vilfo.", + "title": "Vilfo Router" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/translations/de.json b/homeassistant/components/vizio/translations/de.json index ddb68ec09fa..ad0cc604d13 100644 --- a/homeassistant/components/vizio/translations/de.json +++ b/homeassistant/components/vizio/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "cannot_connect": "Verbindungsfehler", + "already_configured_device": "Ger\u00e4t ist bereits konfiguriert", + "cannot_connect": "Verbindung fehlgeschlagen", "updated_entry": "Dieser Eintrag wurde bereits eingerichtet, aber der Name, die Apps und / oder die in der Konfiguration definierten Optionen stimmen nicht mit der zuvor importierten Konfiguration \u00fcberein, sodass der Konfigurationseintrag entsprechend aktualisiert wurde." }, "error": { @@ -10,17 +11,17 @@ "step": { "pair_tv": { "data": { - "pin": "PIN" + "pin": "PIN-Code" }, "description": "Ihr Fernseher sollte einen Code anzeigen. Geben Sie diesen Code in das Formular ein und fahren Sie mit dem n\u00e4chsten Schritt fort, um die Kopplung abzuschlie\u00dfen.", "title": "Schlie\u00dfen Sie den Pairing-Prozess ab" }, "pairing_complete": { - "description": "Ihr VIZIO SmartCast-Ger\u00e4t ist jetzt mit Home Assistant verbunden.", + "description": "Dein Richten Sie das VIZIO SmartCast-Ger\u00e4t ein ist jetzt mit Home Assistant verbunden.", "title": "Kopplung abgeschlossen" }, "pairing_complete_import": { - "description": "Ihr VIZIO SmartCast-Fernseher ist jetzt mit Home Assistant verbunden. \n\n Ihr Zugriffstoken ist '**{access_token}**'.", + "description": "Dein Richten Sie das VIZIO SmartCast-Ger\u00e4t ein ist jetzt mit Home Assistant verbunden.\n\nDein Zugangstoken ist '**{access_token}**'.", "title": "Kopplung abgeschlossen" }, "user": { @@ -30,7 +31,7 @@ "host": "Host", "name": "Name" }, - "description": "Ein Zugriffstoken wird nur f\u00fcr Fernsehger\u00e4te ben\u00f6tigt. Wenn Sie ein Fernsehger\u00e4t konfigurieren und noch kein Zugriffstoken haben, lassen Sie es leer, um einen Pairing-Vorgang durchzuf\u00fchren.", + "description": "Ein Zugangstoken wird nur f\u00fcr Fernsehger\u00e4te ben\u00f6tigt. Wenn du ein Fernsehger\u00e4t konfigurierst und noch kein Zugangstoken hast, lass es leer, um einen Pairing-Vorgang durchzuf\u00fchren.", "title": "Richten Sie das VIZIO SmartCast-Ger\u00e4t ein" } } @@ -44,7 +45,7 @@ "volume_step": "Lautst\u00e4rken-Schrittgr\u00f6\u00dfe" }, "description": "Wenn Sie \u00fcber ein Smart-TV-Ger\u00e4t verf\u00fcgen, k\u00f6nnen Sie Ihre Quellliste optional filtern, indem Sie ausw\u00e4hlen, welche Apps in Ihre Quellliste aufgenommen oder ausgeschlossen werden sollen.", - "title": "Aktualisieren Sie die VIZIO SmartCast-Optionen" + "title": "Aktualisiere die Richten Sie das VIZIO SmartCast-Ger\u00e4t ein-Optionen" } } } diff --git a/homeassistant/components/vizio/translations/tr.json b/homeassistant/components/vizio/translations/tr.json new file mode 100644 index 00000000000..4b923cfb4b3 --- /dev/null +++ b/homeassistant/components/vizio/translations/tr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured_device": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "user": { + "data": { + "access_token": "Eri\u015fim Belirteci", + "host": "Ana Bilgisayar" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/translations/uk.json b/homeassistant/components/vizio/translations/uk.json new file mode 100644 index 00000000000..958307d543f --- /dev/null +++ b/homeassistant/components/vizio/translations/uk.json @@ -0,0 +1,54 @@ +{ + "config": { + "abort": { + "already_configured_device": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "updated_entry": "\u0426\u044f \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u0438\u0439, \u0430\u043b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438, \u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u0456 \u0432 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457, \u043d\u0435 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u0430\u044e\u0442\u044c \u0440\u0430\u043d\u0456\u0448\u0435 \u0456\u043c\u043f\u043e\u0440\u0442\u043e\u0432\u0430\u043d\u0438\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u043d\u044f\u043c, \u0442\u043e\u043c\u0443 \u0437\u0430\u043f\u0438\u0441 \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u0457 \u0431\u0443\u043b\u0430 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u043d\u0438\u043c \u0447\u0438\u043d\u043e\u043c \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0439." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "complete_pairing_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438. \u041f\u0435\u0440\u0448 \u043d\u0456\u0436 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0438 \u0441\u043f\u0440\u043e\u0431\u0443, \u043f\u0435\u0440\u0435\u043a\u043e\u043d\u0430\u0439\u0442\u0435\u0441\u044f, \u0449\u043e \u0432\u0432\u0435\u0434\u0435\u043d\u0438\u0439 \u0412\u0430\u043c\u0438 PIN-\u043a\u043e\u0434 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439, \u0430 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439 \u0456 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0439 \u0434\u043e \u043c\u0435\u0440\u0435\u0436\u0456.", + "existing_config_entry_found": "\u0406\u0441\u043d\u0443\u044e\u0447\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 VIZIO SmartCast \u0437 \u0442\u0430\u043a\u0438\u043c \u0441\u0435\u0440\u0456\u0439\u043d\u0438\u043c \u043d\u043e\u043c\u0435\u0440\u043e\u043c \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e. \u0412\u0438 \u043f\u043e\u0432\u0438\u043d\u043d\u0456 \u0432\u0438\u0434\u0430\u043b\u0438\u0442\u0438 \u0456\u0441\u043d\u0443\u044e\u0447\u0438\u0439 \u0437\u0430\u043f\u0438\u0441, \u0449\u043e\u0431 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 \u043f\u043e\u0442\u043e\u0447\u043d\u0438\u0439." + }, + "step": { + "pair_tv": { + "data": { + "pin": "PIN-\u043a\u043e\u0434" + }, + "description": "\u0412\u0430\u0448 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 \u0437\u0430\u0440\u0430\u0437 \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0432\u0456\u0434\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u0438 \u043a\u043e\u0434. \u0412\u0432\u0435\u0434\u0456\u0442\u044c \u0446\u0435\u0439 \u043a\u043e\u0434 \u0443 \u0444\u043e\u0440\u043c\u0443, \u0430 \u043f\u043e\u0442\u0456\u043c \u043f\u0435\u0440\u0435\u0439\u0434\u0456\u0442\u044c \u0434\u043e \u043d\u0430\u0441\u0442\u0443\u043f\u043d\u043e\u0433\u043e \u043a\u0440\u043e\u043a\u0443, \u0449\u043e\u0431 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438.", + "title": "\u0417\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043d\u044f \u043f\u0440\u043e\u0446\u0435\u0441\u0443 \u0441\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f" + }, + "pairing_complete": { + "description": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 VIZIO SmartCast \u0442\u0435\u043f\u0435\u0440 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0434\u043e Home Assistant.", + "title": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u043e" + }, + "pairing_complete_import": { + "description": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 VIZIO SmartCast \u0442\u0435\u043f\u0435\u0440 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u0434\u043e Home Assistant. \n\n \u0412\u0430\u0448 \u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443 - '** {access_token} **'.", + "title": "\u0421\u043f\u0430\u0440\u044e\u0432\u0430\u043d\u043d\u044f \u0432\u0438\u043a\u043e\u043d\u0430\u043d\u043e" + }, + "user": { + "data": { + "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443", + "device_class": "\u0422\u0438\u043f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e", + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "description": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443 \u043d\u0435\u043e\u0431\u0445\u0456\u0434\u043d\u0438\u0439 \u0442\u0456\u043b\u044c\u043a\u0438 \u0434\u043b\u044f \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440\u0456\u0432. \u042f\u043a\u0449\u043e \u0412\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0443\u0454\u0442\u0435 \u0442\u0435\u043b\u0435\u0432\u0456\u0437\u043e\u0440 \u0456 \u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0443 \u0443 \u0412\u0430\u0441 \u0449\u0435 \u043d\u0435 \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043e, \u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u0446\u0435 \u043f\u043e\u043b\u0435 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c, \u0449\u043e\u0431 \u0432\u0438\u043a\u043e\u043d\u0430\u0442\u0438 \u043f\u0440\u043e\u0446\u0435\u0441 \u0441\u0442\u0432\u043e\u0440\u0435\u043d\u043d\u044f \u043f\u0430\u0440\u0438.", + "title": "VIZIO SmartCast" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "apps_to_include_or_exclude": "\u0421\u043f\u0438\u0441\u043e\u043a \u0434\u0436\u0435\u0440\u0435\u043b", + "include_or_exclude": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u0430\u0431\u043e \u0432\u0438\u043a\u043b\u044e\u0447\u0438\u0442\u0438 \u0434\u0436\u0435\u0440\u0435\u043b\u0430?", + "volume_step": "\u041a\u0440\u043e\u043a \u0433\u0443\u0447\u043d\u043e\u0441\u0442\u0456" + }, + "description": "\u042f\u043a\u0449\u043e \u0443 \u0432\u0430\u0441 \u0454 Smart TV, \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u0440\u0438 \u0431\u0430\u0436\u0430\u043d\u043d\u0456 \u0432\u0456\u0434\u0444\u0456\u043b\u044c\u0442\u0440\u0443\u0432\u0430\u0442\u0438 \u0441\u043f\u0438\u0441\u043e\u043a \u0434\u0436\u0435\u0440\u0435\u043b, \u0432\u043a\u043b\u044e\u0447\u0438\u0432\u0448\u0438 \u0430\u0431\u043e \u0432\u0438\u043a\u043b\u044e\u0447\u0438\u0432\u0448\u0438 \u043f\u0440\u043e\u0433\u0440\u0430\u043c\u0438 \u0437\u0456 \u0441\u043f\u0438\u0441\u043a\u0443.", + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f VIZIO SmartCast" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volkszaehler/manifest.json b/homeassistant/components/volkszaehler/manifest.json index b133550b327..937c589bdf4 100644 --- a/homeassistant/components/volkszaehler/manifest.json +++ b/homeassistant/components/volkszaehler/manifest.json @@ -2,6 +2,6 @@ "domain": "volkszaehler", "name": "Volkszaehler", "documentation": "https://www.home-assistant.io/integrations/volkszaehler", - "requirements": ["volkszaehler==0.1.3"], + "requirements": ["volkszaehler==0.2.1"], "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/volumio/browse_media.py b/homeassistant/components/volumio/browse_media.py index dfceb491372..41330c37473 100644 --- a/homeassistant/components/volumio/browse_media.py +++ b/homeassistant/components/volumio/browse_media.py @@ -88,7 +88,7 @@ def _item_to_media_class(item, parent_item=None): return MEDIA_CLASS_DIRECTORY -def _list_payload(media_library, item, children=None): +def _list_payload(item, children=None): return BrowseMedia( title=item["name"], media_class=MEDIA_CLASS_DIRECTORY, @@ -100,11 +100,13 @@ def _list_payload(media_library, item, children=None): ) -def _raw_item_payload(media_library, item, parent_item=None, title=None, info=None): +def _raw_item_payload(entity, item, parent_item=None, title=None, info=None): if "type" in item: thumbnail = item.get("albumart") if thumbnail: - thumbnail = media_library.canonic_url(thumbnail) + item_hash = str(hash(thumbnail)) + entity.thumbnail_cache.setdefault(item_hash, thumbnail) + thumbnail = entity.get_browse_image_url(MEDIA_TYPE_MUSIC, item_hash) else: # don't use the built-in volumio white-on-white icons thumbnail = None @@ -121,16 +123,14 @@ def _raw_item_payload(media_library, item, parent_item=None, title=None, info=No } -def _item_payload(media_library, item, parent_item): - return BrowseMedia( - **_raw_item_payload(media_library, item, parent_item=parent_item) - ) +def _item_payload(entity, item, parent_item): + return BrowseMedia(**_raw_item_payload(entity, item, parent_item=parent_item)) async def browse_top_level(media_library): """Browse the top-level of a Volumio media hierarchy.""" navigation = await media_library.browse() - children = [_list_payload(media_library, item) for item in navigation["lists"]] + children = [_list_payload(item) for item in navigation["lists"]] return BrowseMedia( media_class=MEDIA_CLASS_DIRECTORY, media_content_id="library", @@ -142,7 +142,7 @@ async def browse_top_level(media_library): ) -async def browse_node(media_library, media_content_type, media_content_id): +async def browse_node(entity, media_library, media_content_type, media_content_id): """Browse a node of a Volumio media hierarchy.""" json_item = json.loads(media_content_id) navigation = await media_library.browse(json_item["uri"]) @@ -152,7 +152,7 @@ async def browse_node(media_library, media_content_type, media_content_id): # we only use the first list since the second one could include all tracks first_list = navigation["lists"][0] children = [ - _item_payload(media_library, item, parent_item=json_item) + _item_payload(entity, item, parent_item=json_item) for item in first_list["items"] ] info = navigation.get("info") @@ -163,5 +163,5 @@ async def browse_node(media_library, media_content_type, media_content_id): else: title = "Media Library" - payload = _raw_item_payload(media_library, json_item, title=title, info=info) + payload = _raw_item_payload(entity, json_item, title=title, info=info) return BrowseMedia(**payload, children=children) diff --git a/homeassistant/components/volumio/media_player.py b/homeassistant/components/volumio/media_player.py index 69790e71732..850f44343c2 100644 --- a/homeassistant/components/volumio/media_player.py +++ b/homeassistant/components/volumio/media_player.py @@ -83,6 +83,7 @@ class Volumio(MediaPlayerEntity): self._state = {} self._playlists = [] self._currentplaylist = None + self.thumbnail_cache = {} async def async_update(self): """Update state.""" @@ -257,7 +258,18 @@ class Volumio(MediaPlayerEntity): async def async_browse_media(self, media_content_type=None, media_content_id=None): """Implement the websocket media browsing helper.""" + self.thumbnail_cache = {} if media_content_type in [None, "library"]: return await browse_top_level(self._volumio) - return await browse_node(self._volumio, media_content_type, media_content_id) + return await browse_node( + self, self._volumio, media_content_type, media_content_id + ) + + async def async_get_browse_image( + self, media_content_type, media_content_id, media_image_id=None + ): + """Get album art from Volumio.""" + cached_url = self.thumbnail_cache.get(media_content_id) + image_url = self._volumio.canonic_url(cached_url) + return await self._async_fetch_image(image_url) diff --git a/homeassistant/components/volumio/translations/de.json b/homeassistant/components/volumio/translations/de.json index ef455299de6..45727d85ee0 100644 --- a/homeassistant/components/volumio/translations/de.json +++ b/homeassistant/components/volumio/translations/de.json @@ -1,7 +1,19 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { - "cannot_connect": "Verbindungsfehler" + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Port" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/volumio/translations/tr.json b/homeassistant/components/volumio/translations/tr.json new file mode 100644 index 00000000000..249bb17d64e --- /dev/null +++ b/homeassistant/components/volumio/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ke\u015ffedilen Volumio'ya ba\u011flan\u0131lam\u0131yor" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/volumio/translations/uk.json b/homeassistant/components/volumio/translations/uk.json index 58947e14e4f..c517eafa2bd 100644 --- a/homeassistant/components/volumio/translations/uk.json +++ b/homeassistant/components/volumio/translations/uk.json @@ -1,14 +1,16 @@ { "config": { "abort": { - "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e" + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u0437'\u0454\u0434\u043d\u0430\u043d\u043d\u044f \u0456\u0437 \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u0438\u043c Volumio." }, "error": { - "cannot_connect": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, "step": { "discovery_confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 Volumio `{name}`?", "title": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e Volumio" }, "user": { diff --git a/homeassistant/components/water_heater/translations/uk.json b/homeassistant/components/water_heater/translations/uk.json new file mode 100644 index 00000000000..d6558828a8e --- /dev/null +++ b/homeassistant/components/water_heater/translations/uk.json @@ -0,0 +1,8 @@ +{ + "device_automation": { + "action_type": { + "turn_off": "{entity_name}: \u0432\u0438\u043c\u043a\u043d\u0443\u0442\u0438", + "turn_on": "{entity_name}: \u0443\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/weather/__init__.py b/homeassistant/components/weather/__init__.py index 8ddcf052e1f..5127dae1102 100644 --- a/homeassistant/components/weather/__init__.py +++ b/homeassistant/components/weather/__init__.py @@ -35,6 +35,7 @@ ATTR_FORECAST = "forecast" ATTR_FORECAST_CONDITION = "condition" ATTR_FORECAST_PRECIPITATION = "precipitation" ATTR_FORECAST_PRECIPITATION_PROBABILITY = "precipitation_probability" +ATTR_FORECAST_PRESSURE = "pressure" ATTR_FORECAST_TEMP = "temperature" ATTR_FORECAST_TEMP_LOW = "templow" ATTR_FORECAST_TIME = "datetime" diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index 80ea945834b..77521c1ed98 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -58,11 +58,11 @@ def handle_subscribe_events(hass, connection, msg): """Handle subscribe events command.""" # Circular dep # pylint: disable=import-outside-toplevel - from .permissions import SUBSCRIBE_WHITELIST + from .permissions import SUBSCRIBE_ALLOWLIST event_type = msg["event_type"] - if event_type not in SUBSCRIBE_WHITELIST and not connection.user.is_admin: + if event_type not in SUBSCRIBE_ALLOWLIST and not connection.user.is_admin: raise Unauthorized if event_type == EVENT_STATE_CHANGED: @@ -132,15 +132,16 @@ async def handle_call_service(hass, connection, msg): blocking = False try: + context = connection.context(msg) await hass.services.async_call( msg["domain"], msg["service"], msg.get("service_data"), blocking, - connection.context(msg), + context, ) connection.send_message( - messages.result_message(msg["id"], {"context": connection.context(msg)}) + messages.result_message(msg["id"], {"context": context}) ) except ServiceNotFound as err: if err.domain == msg["domain"] and err.service == msg["service"]: @@ -155,6 +156,10 @@ async def handle_call_service(hass, connection, msg): msg["id"], const.ERR_HOME_ASSISTANT_ERROR, str(err) ) ) + except vol.Invalid as err: + connection.send_message( + messages.error_message(msg["id"], const.ERR_INVALID_FORMAT, str(err)) + ) except HomeAssistantError as err: connection.logger.exception(err) connection.send_message( diff --git a/homeassistant/components/websocket_api/permissions.py b/homeassistant/components/websocket_api/permissions.py index 8b00981fb04..010a18f972c 100644 --- a/homeassistant/components/websocket_api/permissions.py +++ b/homeassistant/components/websocket_api/permissions.py @@ -22,7 +22,7 @@ from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED # These are events that do not contain any sensitive data # Except for state_changed, which is handled accordingly. -SUBSCRIBE_WHITELIST = { +SUBSCRIBE_ALLOWLIST = { EVENT_AREA_REGISTRY_UPDATED, EVENT_COMPONENT_LOADED, EVENT_CORE_CONFIG_UPDATE, diff --git a/homeassistant/components/wemo/manifest.json b/homeassistant/components/wemo/manifest.json index dc04926004a..fe5559b58d6 100644 --- a/homeassistant/components/wemo/manifest.json +++ b/homeassistant/components/wemo/manifest.json @@ -3,7 +3,7 @@ "name": "Belkin WeMo", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wemo", - "requirements": ["pywemo==0.5.6"], + "requirements": ["pywemo==0.6.1"], "ssdp": [ { "manufacturer": "Belkin International Inc." diff --git a/homeassistant/components/wemo/translations/de.json b/homeassistant/components/wemo/translations/de.json index f20ad5598ab..81694f65ea2 100644 --- a/homeassistant/components/wemo/translations/de.json +++ b/homeassistant/components/wemo/translations/de.json @@ -2,7 +2,7 @@ "config": { "abort": { "no_devices_found": "Es wurden keine Wemo-Ger\u00e4te im Netzwerk gefunden.", - "single_instance_allowed": "Nur eine einzige Konfiguration von Wemo ist zul\u00e4ssig." + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "step": { "confirm": { diff --git a/homeassistant/components/wemo/translations/tr.json b/homeassistant/components/wemo/translations/tr.json index 411a536ceed..a87d832eece 100644 --- a/homeassistant/components/wemo/translations/tr.json +++ b/homeassistant/components/wemo/translations/tr.json @@ -1,7 +1,13 @@ { "config": { "abort": { - "no_devices_found": "A\u011fda Wemo cihaz\u0131 bulunamad\u0131." + "no_devices_found": "A\u011fda Wemo cihaz\u0131 bulunamad\u0131.", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "confirm": { + "description": "Wemo'yu kurmak istiyor musunuz?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/wemo/translations/uk.json b/homeassistant/components/wemo/translations/uk.json new file mode 100644 index 00000000000..1217d664234 --- /dev/null +++ b/homeassistant/components/wemo/translations/uk.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "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." + }, + "step": { + "confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u0442\u0438 Wemo?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/tr.json b/homeassistant/components/wiffi/translations/tr.json new file mode 100644 index 00000000000..26ec2e61e00 --- /dev/null +++ b/homeassistant/components/wiffi/translations/tr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "addr_in_use": "Sunucu ba\u011flant\u0131 noktas\u0131 zaten kullan\u0131l\u0131yor.", + "start_server_failed": "Ba\u015flatma sunucusu ba\u015far\u0131s\u0131z oldu." + }, + "step": { + "user": { + "data": { + "port": "Port" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "Zaman a\u015f\u0131m\u0131 (dakika)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wiffi/translations/uk.json b/homeassistant/components/wiffi/translations/uk.json new file mode 100644 index 00000000000..dc8dac9cd56 --- /dev/null +++ b/homeassistant/components/wiffi/translations/uk.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "addr_in_use": "\u041f\u043e\u0440\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0432\u0436\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f.", + "start_server_failed": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u0438 \u0441\u0435\u0440\u0432\u0435\u0440." + }, + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f TCP-\u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0434\u043b\u044f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432 WIFFI" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 (\u0432 \u0445\u0432\u0438\u043b\u0438\u043d\u0430\u0445)" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wilight/__init__.py b/homeassistant/components/wilight/__init__.py index 8821190bd32..97b48257103 100644 --- a/homeassistant/components/wilight/__init__.py +++ b/homeassistant/components/wilight/__init__.py @@ -10,7 +10,7 @@ from .const import DOMAIN from .parent_device import WiLightParent # List the platforms that you want to support. -PLATFORMS = ["light"] +PLATFORMS = ["fan", "light"] async def async_setup(hass: HomeAssistant, config: dict): diff --git a/homeassistant/components/wilight/config_flow.py b/homeassistant/components/wilight/config_flow.py index 6bc81e363aa..3f1b12395ba 100644 --- a/homeassistant/components/wilight/config_flow.py +++ b/homeassistant/components/wilight/config_flow.py @@ -15,7 +15,7 @@ CONF_MODEL_NAME = "model_name" WILIGHT_MANUFACTURER = "All Automacao Ltda" # List the components supported by this integration. -ALLOWED_WILIGHT_COMPONENTS = ["light"] +ALLOWED_WILIGHT_COMPONENTS = ["light", "fan"] class WiLightFlowHandler(ConfigFlow, domain=DOMAIN): diff --git a/homeassistant/components/wilight/fan.py b/homeassistant/components/wilight/fan.py new file mode 100644 index 00000000000..6d8ad88d6c0 --- /dev/null +++ b/homeassistant/components/wilight/fan.py @@ -0,0 +1,122 @@ +"""Support for WiLight Fan.""" + +from pywilight.const import ( + DOMAIN, + FAN_V1, + ITEM_FAN, + WL_DIRECTION_FORWARD, + WL_DIRECTION_OFF, + WL_DIRECTION_REVERSE, + WL_SPEED_HIGH, + WL_SPEED_LOW, + WL_SPEED_MEDIUM, +) + +from homeassistant.components.fan import ( + DIRECTION_FORWARD, + SPEED_HIGH, + SPEED_LOW, + SPEED_MEDIUM, + SUPPORT_DIRECTION, + SUPPORT_SET_SPEED, + FanEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from . import WiLightDevice + +SUPPORTED_SPEEDS = [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + +SUPPORTED_FEATURES = SUPPORT_SET_SPEED | SUPPORT_DIRECTION + + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +): + """Set up WiLight lights from a config entry.""" + parent = hass.data[DOMAIN][entry.entry_id] + + # Handle a discovered WiLight device. + entities = [] + for item in parent.api.items: + if item["type"] != ITEM_FAN: + continue + index = item["index"] + item_name = item["name"] + if item["sub_type"] != FAN_V1: + continue + entity = WiLightFan(parent.api, index, item_name) + entities.append(entity) + + async_add_entities(entities) + + +class WiLightFan(WiLightDevice, FanEntity): + """Representation of a WiLights fan.""" + + def __init__(self, api_device, index, item_name): + """Initialize the device.""" + super().__init__(api_device, index, item_name) + # Initialize the WiLights fan. + self._direction = WL_DIRECTION_FORWARD + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORTED_FEATURES + + @property + def icon(self): + """Return the icon of device based on its type.""" + return "mdi:fan" + + @property + def is_on(self): + """Return true if device is on.""" + return self._status.get("direction", WL_DIRECTION_OFF) != WL_DIRECTION_OFF + + @property + def speed(self) -> str: + """Return the current speed.""" + return self._status.get("speed", SPEED_HIGH) + + @property + def speed_list(self) -> list: + """Get the list of available speeds.""" + return SUPPORTED_SPEEDS + + @property + def current_direction(self) -> str: + """Return the current direction of the fan.""" + if "direction" in self._status: + if self._status["direction"] != WL_DIRECTION_OFF: + self._direction = self._status["direction"] + return self._direction + + async def async_turn_on(self, speed: str = None, **kwargs): + """Turn on the fan.""" + if speed is None: + await self._client.set_fan_direction(self._index, self._direction) + else: + await self.async_set_speed(speed) + + async def async_set_speed(self, speed: str): + """Set the speed of the fan.""" + wl_speed = WL_SPEED_HIGH + if speed == SPEED_LOW: + wl_speed = WL_SPEED_LOW + if speed == SPEED_MEDIUM: + wl_speed = WL_SPEED_MEDIUM + await self._client.set_fan_speed(self._index, wl_speed) + + async def async_set_direction(self, direction: str): + """Set the direction of the fan.""" + wl_direction = WL_DIRECTION_REVERSE + if direction == DIRECTION_FORWARD: + wl_direction = WL_DIRECTION_FORWARD + await self._client.set_fan_direction(self._index, wl_direction) + + async def async_turn_off(self, **kwargs): + """Turn the fan off.""" + await self._client.set_fan_direction(self._index, WL_DIRECTION_OFF) diff --git a/homeassistant/components/wilight/manifest.json b/homeassistant/components/wilight/manifest.json index bb20da2b1ce..c9f4fb049fc 100644 --- a/homeassistant/components/wilight/manifest.json +++ b/homeassistant/components/wilight/manifest.json @@ -3,7 +3,7 @@ "name": "WiLight", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wilight", - "requirements": ["pywilight==0.0.65"], + "requirements": ["pywilight==0.0.66"], "ssdp": [ { "manufacturer": "All Automacao Ltda" diff --git a/homeassistant/components/wilight/parent_device.py b/homeassistant/components/wilight/parent_device.py index a53bc352a7b..faf71b74f72 100644 --- a/homeassistant/components/wilight/parent_device.py +++ b/homeassistant/components/wilight/parent_device.py @@ -84,11 +84,9 @@ class WiLightParent: async def async_reset(self): """Reset api.""" - # If the initialization was wrong. - if self._api is None: - return True - - self._api.client.stop() + # If the initialization was not wrong. + if self._api is not None: + self._api.client.stop() def create_api_device(host): diff --git a/homeassistant/components/wilight/translations/de.json b/homeassistant/components/wilight/translations/de.json index 07d00495af7..d56e782279a 100644 --- a/homeassistant/components/wilight/translations/de.json +++ b/homeassistant/components/wilight/translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "flow_title": "WiLight: {name}", "step": { "confirm": { diff --git a/homeassistant/components/wilight/translations/tr.json b/homeassistant/components/wilight/translations/tr.json new file mode 100644 index 00000000000..5307276a71d --- /dev/null +++ b/homeassistant/components/wilight/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wilight/translations/uk.json b/homeassistant/components/wilight/translations/uk.json new file mode 100644 index 00000000000..7517538499e --- /dev/null +++ b/homeassistant/components/wilight/translations/uk.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "not_supported_device": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0430\u0440\u0430\u0437\u0456 \u043d\u0435 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454\u0442\u044c\u0441\u044f.", + "not_wilight_device": "\u0426\u0435 \u043d\u0435 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 WiLight." + }, + "flow_title": "WiLight: {name}", + "step": { + "confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 WiLight {name}? \n\n \u0426\u0435 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0456\u0434\u0442\u0440\u0438\u043c\u0443\u0454: {components}", + "title": "WiLight" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wink/manifest.json b/homeassistant/components/wink/manifest.json index a1bae648292..7d357d88e55 100644 --- a/homeassistant/components/wink/manifest.json +++ b/homeassistant/components/wink/manifest.json @@ -2,7 +2,7 @@ "domain": "wink", "name": "Wink", "documentation": "https://www.home-assistant.io/integrations/wink", - "requirements": ["pubnubsub-handler==1.0.8", "python-wink==1.10.5"], + "requirements": ["pubnubsub-handler==1.0.9", "python-wink==1.10.5"], "dependencies": ["configurator", "http"], "codeowners": [] } diff --git a/homeassistant/components/withings/translations/de.json b/homeassistant/components/withings/translations/de.json index d217640e44b..05d3795a0b0 100644 --- a/homeassistant/components/withings/translations/de.json +++ b/homeassistant/components/withings/translations/de.json @@ -1,22 +1,30 @@ { "config": { "abort": { - "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Autorisierungs-URL.", - "missing_configuration": "Die Withings-Integration ist nicht konfiguriert. Bitte folgen Sie der Dokumentation." + "already_configured": "Konfiguration des Profils aktualisiert.", + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", + "no_url_available": "Keine URL verf\u00fcgbar. Informationen zu diesem Fehler findest du [im Hilfebereich]({docs_url})." }, "create_entry": { "default": "Erfolgreiche Authentifizierung mit Withings." }, + "error": { + "already_configured": "Konto wurde bereits konfiguriert" + }, "step": { "pick_implementation": { - "title": "Authentifizierungsmethode ausw\u00e4hlen" + "title": "W\u00e4hle die Authentifizierungsmethode" }, "profile": { "data": { - "profile": "Profil" + "profile": "Profilname" }, "description": "Welches Profil hast du auf der Withings-Website ausgew\u00e4hlt? Es ist wichtig, dass die Profile \u00fcbereinstimmen, da sonst die Daten falsch beschriftet werden.", "title": "Benutzerprofil" + }, + "reauth": { + "title": "Integration erneut authentifizieren" } } } diff --git a/homeassistant/components/withings/translations/fr.json b/homeassistant/components/withings/translations/fr.json index 017a9e63078..b5f524698f5 100644 --- a/homeassistant/components/withings/translations/fr.json +++ b/homeassistant/components/withings/translations/fr.json @@ -10,7 +10,7 @@ "default": "Authentifi\u00e9 avec succ\u00e8s \u00e0 Withings pour le profil s\u00e9lectionn\u00e9." }, "error": { - "already_configured": "Le compte a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9" + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" }, "flow_title": "Withings: {profile}", "step": { diff --git a/homeassistant/components/withings/translations/tr.json b/homeassistant/components/withings/translations/tr.json new file mode 100644 index 00000000000..4e0228708ea --- /dev/null +++ b/homeassistant/components/withings/translations/tr.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Profil i\u00e7in yap\u0131land\u0131rma g\u00fcncellendi." + }, + "error": { + "already_configured": "Hesap zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "step": { + "profile": { + "data": { + "profile": "Profil Ad\u0131" + }, + "title": "Kullan\u0131c\u0131 profili." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/withings/translations/uk.json b/homeassistant/components/withings/translations/uk.json new file mode 100644 index 00000000000..5efc27042b1 --- /dev/null +++ b/homeassistant/components/withings/translations/uk.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "\u041e\u043d\u043e\u0432\u043b\u0435\u043d\u043e \u043a\u043e\u043d\u0444\u0456\u0433\u0443\u0440\u0430\u0446\u0456\u044e \u043f\u0440\u043e\u0444\u0456\u043b\u044e.", + "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", + "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430. \u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0456\u0454\u044e] ({docs_url}) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u0446\u044e \u043f\u043e\u043c\u0438\u043b\u043a\u0443." + }, + "create_entry": { + "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." + }, + "error": { + "already_configured": "\u0426\u0435\u0439 \u043e\u0431\u043b\u0456\u043a\u043e\u0432\u0438\u0439 \u0437\u0430\u043f\u0438\u0441 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "flow_title": "Withings: {profile}", + "step": { + "pick_implementation": { + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + }, + "profile": { + "data": { + "profile": "\u041d\u0430\u0437\u0432\u0430 \u043f\u0440\u043e\u0444\u0456\u043b\u044e" + }, + "description": "\u0412\u043a\u0430\u0436\u0456\u0442\u044c \u0443\u043d\u0456\u043a\u0430\u043b\u044c\u043d\u0435 \u0456\u043c'\u044f \u043f\u0440\u043e\u0444\u0456\u043b\u044e \u0434\u043b\u044f \u0446\u0438\u0445 \u0434\u0430\u043d\u0438\u0445. \u042f\u043a \u043f\u0440\u0430\u0432\u0438\u043b\u043e, \u0446\u0435 \u043d\u0430\u0437\u0432\u0430, \u043e\u0431\u0440\u0430\u043d\u0430 \u043d\u0430 \u043f\u043e\u043f\u0435\u0440\u0435\u0434\u043d\u044c\u043e\u043c\u0443 \u043a\u0440\u043e\u0446\u0456.", + "title": "Withings" + }, + "reauth": { + "description": "\u041f\u0440\u043e\u0444\u0456\u043b\u044c \"{profile}\" \u043f\u043e\u0432\u0438\u043d\u0435\u043d \u0431\u0443\u0442\u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u043e\u0432\u0430\u043d\u0438\u0439 \u0434\u043b\u044f \u043f\u0440\u043e\u0434\u043e\u0432\u0436\u0435\u043d\u043d\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0434\u0430\u043d\u0438\u0445 Withings.", + "title": "\u041f\u043e\u0432\u0442\u043e\u0440\u043d\u043e \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0443\u0432\u0430\u0442\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u044e" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/translations/de.json b/homeassistant/components/wled/translations/de.json index ff12e429bd6..0dd13f763d6 100644 --- a/homeassistant/components/wled/translations/de.json +++ b/homeassistant/components/wled/translations/de.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_configured": "Dieses WLED-Ger\u00e4t ist bereits konfiguriert." + "already_configured": "Dieses WLED-Ger\u00e4t ist bereits konfiguriert.", + "cannot_connect": "Verbindung fehlgeschlagen" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" }, "flow_title": "WLED: {name}", "step": { diff --git a/homeassistant/components/wled/translations/tr.json b/homeassistant/components/wled/translations/tr.json new file mode 100644 index 00000000000..f02764c8aba --- /dev/null +++ b/homeassistant/components/wled/translations/tr.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "Ana Bilgisayar" + }, + "description": "WLED'inizi Home Assistant ile t\u00fcmle\u015ftirmek i\u00e7in ayarlay\u0131n." + }, + "zeroconf_confirm": { + "description": "Home Assistant'a '{name}' adl\u0131 WLED'i eklemek istiyor musunuz?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wled/translations/uk.json b/homeassistant/components/wled/translations/uk.json new file mode 100644 index 00000000000..c0280d33993 --- /dev/null +++ b/homeassistant/components/wled/translations/uk.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "flow_title": "WLED: {name}", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437 WLED." + }, + "zeroconf_confirm": { + "description": "\u0412\u0438 \u0432\u043f\u0435\u0432\u043d\u0435\u043d\u0456, \u0449\u043e \u0445\u043e\u0447\u0435\u0442\u0435 \u0434\u043e\u0434\u0430\u0442\u0438 WLED `{name}`?", + "title": "\u0412\u0438\u044f\u0432\u043b\u0435\u043d\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 WLED" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/de.json b/homeassistant/components/wolflink/translations/de.json index cb7e571d1e6..71f48a6413d 100644 --- a/homeassistant/components/wolflink/translations/de.json +++ b/homeassistant/components/wolflink/translations/de.json @@ -1,5 +1,13 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, "step": { "device": { "data": { diff --git a/homeassistant/components/wolflink/translations/sensor.de.json b/homeassistant/components/wolflink/translations/sensor.de.json index 373e1989578..9680716cd19 100644 --- a/homeassistant/components/wolflink/translations/sensor.de.json +++ b/homeassistant/components/wolflink/translations/sensor.de.json @@ -1,6 +1,7 @@ { "state": { "wolflink__state": { + "permanent": "Permanent", "solarbetrieb": "Solarmodus", "sparbetrieb": "Sparmodus", "sparen": "Sparen", diff --git a/homeassistant/components/wolflink/translations/sensor.tr.json b/homeassistant/components/wolflink/translations/sensor.tr.json index 8b2eb0a8c53..4b1e2778af1 100644 --- a/homeassistant/components/wolflink/translations/sensor.tr.json +++ b/homeassistant/components/wolflink/translations/sensor.tr.json @@ -1,10 +1,19 @@ { "state": { "wolflink__state": { + "glt_betrieb": "BMS modu", + "heizbetrieb": "Is\u0131tma modu", + "kalibration_heizbetrieb": "Is\u0131tma modu kalibrasyonu", + "kalibration_kombibetrieb": "Kombi modu kalibrasyonu", + "reduzierter_betrieb": "S\u0131n\u0131rl\u0131 mod", + "solarbetrieb": "G\u00fcne\u015f modu", + "sparbetrieb": "Ekonomi modu", "standby": "Bekleme", "start": "Ba\u015flat", "storung": "Hata", - "test": "Test" + "test": "Test", + "urlaubsmodus": "Tatil modu", + "warmwasserbetrieb": "DHW modu" } } } \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/sensor.uk.json b/homeassistant/components/wolflink/translations/sensor.uk.json index 665ff99992c..c8a69f2c007 100644 --- a/homeassistant/components/wolflink/translations/sensor.uk.json +++ b/homeassistant/components/wolflink/translations/sensor.uk.json @@ -1,15 +1,87 @@ { "state": { "wolflink__state": { + "1_x_warmwasser": "1 \u0445 \u0413\u0412\u041f", + "abgasklappe": "\u0417\u0430\u0441\u043b\u0456\u043d\u043a\u0430 \u0434\u0438\u043c\u043e\u0432\u0438\u0445 \u0433\u0430\u0437\u0456\u0432", + "absenkbetrieb": "\u0420\u0435\u0436\u0438\u043c \u0430\u0432\u0430\u0440\u0456\u0457", + "absenkstop": "\u0410\u0432\u0430\u0440\u0456\u0439\u043d\u0430 \u0437\u0443\u043f\u0438\u043d\u043a\u0430", + "aktiviert": "\u0410\u043a\u0442\u0438\u0432\u043e\u0432\u0430\u043d\u043e", + "antilegionellenfunktion": "\u0424\u0443\u043d\u043a\u0446\u0456\u044f \u0430\u043d\u0442\u0438-\u043b\u0435\u0433\u0438\u043e\u043d\u0435\u043b\u043b\u0438", + "at_abschaltung": "\u041e\u0422 \u0432\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f", + "at_frostschutz": "\u041e\u0422 \u0437\u0430\u0445\u0438\u0441\u0442 \u0432\u0456\u0434 \u0437\u0430\u043c\u0435\u0440\u0437\u0430\u043d\u043d\u044f", + "aus": "\u0412\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e", + "auto": "\u0410\u0432\u0442\u043e", + "auto_off_cool": "AutoOffCool", + "auto_on_cool": "AutoOnCool", + "automatik_aus": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0435 \u0432\u0438\u043c\u043a\u043d\u0435\u043d\u043d\u044f", + "automatik_ein": "\u0410\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u0435 \u0432\u043c\u0438\u043a\u0430\u043d\u043d\u044f", + "bereit_keine_ladung": "\u0413\u043e\u0442\u043e\u0432\u0438\u0439, \u043d\u0435 \u0437\u0430\u0432\u0430\u043d\u0442\u0430\u0436\u0443\u0454\u0442\u044c\u0441\u044f", + "betrieb_ohne_brenner": "\u0420\u043e\u0431\u043e\u0442\u0430 \u0431\u0435\u0437 \u043f\u0430\u043b\u044c\u043d\u0438\u043a\u0430", + "cooling": "\u041e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f", + "deaktiviert": "\u041d\u0435 \u0430\u043a\u0442\u0438\u0432\u043d\u043e", + "dhw_prior": "DHWPrior", + "eco": "\u0415\u043a\u043e", + "ein": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e", + "estrichtrocknung": "\u0421\u0443\u0448\u0456\u043d\u043d\u044f", + "externe_deaktivierung": "\u0417\u043e\u0432\u043d\u0456\u0448\u043d\u044f \u0434\u0435\u0430\u043a\u0442\u0438\u0432\u0430\u0446\u0456\u044f", + "fernschalter_ein": "\u0414\u0438\u0441\u0442\u0430\u043d\u0446\u0456\u0439\u043d\u0435 \u043a\u0435\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u0443\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043e", + "frost_heizkreis": "\u0417\u0430\u043c\u0435\u0440\u0437\u0430\u043d\u043d\u044f \u043a\u043e\u043d\u0442\u0443\u0440\u0443 \u043e\u043f\u0430\u043b\u0435\u043d\u043d\u044f", + "frost_warmwasser": "\u0417\u0430\u043c\u0435\u0440\u0437\u0430\u043d\u043d\u044f \u0413\u0412\u041f", + "frostschutz": "\u0417\u0430\u0445\u0438\u0441\u0442 \u0432\u0456\u0434 \u0437\u0430\u043c\u0435\u0440\u0437\u0430\u043d\u043d\u044f", + "gasdruck": "\u0422\u0438\u0441\u043a \u0433\u0430\u0437\u0443", + "glt_betrieb": "\u0420\u0435\u0436\u0438\u043c BMS", + "gradienten_uberwachung": "\u0413\u0440\u0430\u0434\u0456\u0454\u043d\u0442\u043d\u0438\u0439 \u043c\u043e\u043d\u0456\u0442\u043e\u0440\u0438\u043d\u0433", + "heizbetrieb": "\u0420\u0435\u0436\u0438\u043c \u043e\u043f\u0430\u043b\u0435\u043d\u043d\u044f", + "heizgerat_mit_speicher": "\u041a\u043e\u0442\u0435\u043b \u0437 \u0446\u0438\u043b\u0456\u043d\u0434\u0440\u043e\u043c", + "heizung": "\u041e\u0431\u0456\u0433\u0440\u0456\u0432", + "initialisierung": "\u0406\u043d\u0456\u0446\u0456\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u044f", + "kalibration": "\u041a\u0430\u043b\u0456\u0431\u0440\u0443\u0432\u0430\u043d\u043d\u044f", + "kalibration_heizbetrieb": "\u041a\u0430\u043b\u0456\u0431\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u0440\u0435\u0436\u0438\u043c\u0443 \u043e\u043f\u0430\u043b\u0435\u043d\u043d\u044f", + "kalibration_kombibetrieb": "\u041a\u0430\u043b\u0456\u0431\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u0432 \u043a\u043e\u043c\u0431\u0456\u043d\u043e\u0432\u0430\u043d\u043e\u043c\u0443 \u0440\u0435\u0436\u0438\u043c\u0456", + "kalibration_warmwasserbetrieb": "\u041a\u0430\u043b\u0456\u0431\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u0413\u0412\u041f", + "kaskadenbetrieb": "\u041a\u0430\u0441\u043a\u0430\u0434\u043d\u0430 \u043e\u043f\u0435\u0440\u0430\u0446\u0456\u044f", + "kombibetrieb": "\u041a\u043e\u043c\u0431\u0456\u043d\u043e\u0432\u0430\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c", + "kombigerat": "\u0414\u0432\u043e\u043a\u043e\u043d\u0442\u0443\u0440\u043d\u0438\u0439 \u043a\u043e\u0442\u0435\u043b", + "kombigerat_mit_solareinbindung": "\u0414\u0432\u043e\u043a\u043e\u043d\u0442\u0443\u0440\u043d\u0438\u0439 \u043a\u043e\u0442\u0435\u043b \u0437 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0454\u044e \u0441\u043e\u043d\u044f\u0447\u043d\u043e\u0457 \u0441\u0438\u0441\u0442\u0435\u043c\u0438", + "mindest_kombizeit": "\u041c\u0456\u043d\u0456\u043c\u0430\u043b\u044c\u043d\u0438\u0439 \u043a\u043e\u043c\u0431\u0456\u043d\u043e\u0432\u0430\u043d\u0438\u0439 \u0447\u0430\u0441", + "nachlauf_heizkreispumpe": "\u0420\u043e\u0431\u043e\u0442\u0430 \u043d\u0430\u0441\u043e\u0441\u0430 \u043a\u043e\u043d\u0442\u0443\u0440\u0443 \u043e\u043f\u0430\u043b\u0435\u043d\u043d\u044f", + "nachspulen": "\u041f\u043e\u0441\u0442-\u043f\u0440\u043e\u043c\u0438\u0432\u043a\u0430", + "nur_heizgerat": "\u0422\u0456\u043b\u044c\u043a\u0438 \u0431\u043e\u0439\u043b\u0435\u0440", + "parallelbetrieb": "\u041f\u0430\u0440\u0430\u043b\u0435\u043b\u044c\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c", + "partymodus": "\u0420\u0435\u0436\u0438\u043c \u0432\u0435\u0447\u0456\u0440\u043a\u0438", + "perm_cooling": "\u041f\u043e\u0441\u0442\u0456\u0439\u043d\u0435 \u043e\u0445\u043e\u043b\u043e\u0434\u0436\u0435\u043d\u043d\u044f", "permanent": "\u041f\u043e\u0441\u0442\u0456\u0439\u043d\u043e", + "permanentbetrieb": "\u041f\u043e\u0441\u0442\u0456\u0439\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c", + "reduzierter_betrieb": "\u041e\u0431\u043c\u0435\u0436\u0435\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c", + "rt_abschaltung": "RT \u0432\u0438\u043c\u0438\u043a\u0430\u043d\u043d\u044f", + "rt_frostschutz": "RT \u0437\u0430\u0445\u0438\u0441\u0442 \u0432\u0456\u0434 \u0437\u0430\u043c\u0435\u0440\u0437\u0430\u043d\u043d\u044f", + "ruhekontakt": "\u0420\u0435\u0448\u0442\u0430 \u043a\u043e\u043d\u0442\u0430\u043a\u0442\u0456\u0432", + "schornsteinfeger": "\u0422\u0435\u0441\u0442 \u043d\u0430 \u0432\u0438\u043a\u0438\u0434\u0438", + "smart_grid": "\u0420\u043e\u0437\u0443\u043c\u043d\u0430 \u043c\u0435\u0440\u0435\u0436\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043f\u043e\u0441\u0442\u0430\u0447\u0430\u043d\u043d\u044f", "smart_home": "\u0420\u043e\u0437\u0443\u043c\u043d\u0438\u0439 \u0434\u0456\u043c", + "softstart": "\u041c'\u044f\u043a\u0438\u0439 \u0441\u0442\u0430\u0440\u0442", + "solarbetrieb": "\u0421\u043e\u043d\u044f\u0447\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c", + "sparbetrieb": "\u0420\u0435\u0436\u0438\u043c \u0435\u043a\u043e\u043d\u043e\u043c\u0456\u0457", "sparen": "\u0415\u043a\u043e\u043d\u043e\u043c\u0456\u044f", + "spreizung_hoch": "dT \u0437\u0430\u043d\u0430\u0434\u0442\u043e \u0448\u0438\u0440\u043e\u043a\u0438\u0439", + "spreizung_kf": "\u0421\u043f\u0440\u0435\u0434 KF", "stabilisierung": "\u0421\u0442\u0430\u0431\u0456\u043b\u0456\u0437\u0430\u0446\u0456\u044f", "standby": "\u041e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u043d\u044f", - "start": "\u041f\u043e\u0447\u0430\u0442\u043e\u043a", + "start": "\u0417\u0430\u043f\u0443\u0441\u043a", "storung": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430", - "taktsperre": "\u0410\u043d\u0442\u0438\u0446\u0438\u043a\u043b", - "test": "\u0422\u0435\u0441\u0442" + "taktsperre": "\u0410\u043d\u0442\u0438-\u0446\u0438\u043a\u043b", + "telefonfernschalter": "\u0414\u0438\u0441\u0442\u0430\u043d\u0446\u0456\u0439\u043d\u0435 \u043a\u0435\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u0437 \u0442\u0435\u043b\u0435\u0444\u043e\u043d\u0443", + "test": "\u0422\u0435\u0441\u0442", + "tpw": "TPW", + "urlaubsmodus": "\u0420\u0435\u0436\u0438\u043c \"\u0432\u0438\u0445\u0456\u0434\u043d\u0456\"", + "ventilprufung": "\u0422\u0435\u0441\u0442 \u043a\u043b\u0430\u043f\u0430\u043d\u0430", + "vorspulen": "\u041f\u0440\u043e\u043c\u0438\u0432\u0430\u043d\u043d\u044f \u0432\u0445\u043e\u0434\u0443", + "warmwasser": "\u0413\u0412\u041f", + "warmwasser_schnellstart": "\u0428\u0432\u0438\u0434\u043a\u0438\u0439 \u0437\u0430\u043f\u0443\u0441\u043a \u0413\u0412\u041f", + "warmwasserbetrieb": "\u0420\u0435\u0436\u0438\u043c \u0413\u0412\u041f", + "warmwassernachlauf": "\u0417\u0430\u043f\u0443\u0441\u043a \u0413\u0412\u041f", + "warmwasservorrang": "\u041f\u0440\u0456\u043e\u0440\u0438\u0442\u0435\u0442 \u0413\u0412\u041f", + "zunden": "\u0417\u0430\u043f\u0430\u043b\u044e\u0432\u0430\u043d\u043d\u044f" } } } \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/tr.json b/homeassistant/components/wolflink/translations/tr.json new file mode 100644 index 00000000000..6ed28a58c79 --- /dev/null +++ b/homeassistant/components/wolflink/translations/tr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama", + "unknown": "Beklenmeyen hata" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wolflink/translations/uk.json b/homeassistant/components/wolflink/translations/uk.json index a7fbdfff913..3fdf20a6ace 100644 --- a/homeassistant/components/wolflink/translations/uk.json +++ b/homeassistant/components/wolflink/translations/uk.json @@ -1,17 +1,26 @@ { "config": { "abort": { - "already_configured": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u043d\u0430\u043b\u0430\u0448\u0442\u043e\u0432\u0430\u043d\u043e" + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." }, "error": { "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f.", "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" }, "step": { "device": { "data": { "device_name": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" - } + }, + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 WOLF" + }, + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430" + }, + "title": "WOLF SmartSet" } } } diff --git a/homeassistant/components/xbox/translations/de.json b/homeassistant/components/xbox/translations/de.json index c67f3a49ea4..04f32e05f8b 100644 --- a/homeassistant/components/xbox/translations/de.json +++ b/homeassistant/components/xbox/translations/de.json @@ -1,5 +1,10 @@ { "config": { + "abort": { + "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Authorisierungs-URL.", + "missing_configuration": "Die Komponente ist nicht konfiguriert. Bitte der Dokumentation folgen.", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." + }, "create_entry": { "default": "Erfolgreich authentifiziert" }, diff --git a/homeassistant/components/xbox/translations/lb.json b/homeassistant/components/xbox/translations/lb.json index d305909389f..b83b6d0a499 100644 --- a/homeassistant/components/xbox/translations/lb.json +++ b/homeassistant/components/xbox/translations/lb.json @@ -1,8 +1,12 @@ { "config": { "abort": { + "authorize_url_timeout": "Z\u00e4itiwwerschreidung beim erstellen vun der Authorisatiouns URL.", "missing_configuration": "Komponent net konfigur\u00e9iert. Folleg w.e.g der Dokumentatioun.", "single_instance_allowed": "Scho konfigur\u00e9iert. N\u00ebmmen eng eenzeg Konfiguratioun m\u00e9iglech." + }, + "create_entry": { + "default": "Erfollegr\u00e4ich authentifiz\u00e9iert" } } } \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/tr.json b/homeassistant/components/xbox/translations/tr.json new file mode 100644 index 00000000000..a152eb19468 --- /dev/null +++ b/homeassistant/components/xbox/translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xbox/translations/uk.json b/homeassistant/components/xbox/translations/uk.json new file mode 100644 index 00000000000..a1b3f8340fc --- /dev/null +++ b/homeassistant/components/xbox/translations/uk.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "authorize_url_timeout": "\u041c\u0438\u043d\u0443\u0432 \u0447\u0430\u0441 \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0456\u0457 \u043f\u043e\u0441\u0438\u043b\u0430\u043d\u043d\u044f \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0456\u0457.", + "missing_configuration": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f. \u0411\u0443\u0434\u044c \u043b\u0430\u0441\u043a\u0430, \u043e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438.", + "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." + }, + "create_entry": { + "default": "\u0410\u0432\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044e \u0443\u0441\u043f\u0456\u0448\u043d\u043e \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043d\u043e." + }, + "step": { + "pick_implementation": { + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0441\u043f\u043e\u0441\u0456\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u0457" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/de.json b/homeassistant/components/xiaomi_aqara/translations/de.json index f86868987a0..6b0e25dfcd5 100644 --- a/homeassistant/components/xiaomi_aqara/translations/de.json +++ b/homeassistant/components/xiaomi_aqara/translations/de.json @@ -1,5 +1,14 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt" + }, + "error": { + "discovery_error": "Es konnte kein Xiaomi Aqara Gateway gefunden werden, versuche die IP von Home Assistant als Interface zu nutzen", + "invalid_host": "Ung\u00fcltiger Hostname oder IP-Adresse, schau unter https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem", + "invalid_mac": "Ung\u00fcltige MAC-Adresse" + }, "flow_title": "Xiaomi Aqara Gateway: {name}", "step": { "select": { @@ -8,7 +17,11 @@ } }, "user": { - "description": "Stellen Sie eine Verbindung zu Ihrem Xiaomi Aqara Gateway her. Wenn die IP- und Mac-Adressen leer bleiben, wird die automatische Erkennung verwendet", + "data": { + "host": "IP-Adresse", + "mac": "MAC-Adresse" + }, + "description": "Stelle eine Verbindung zu deinem Xiaomi Aqara Gateway her. Wenn die IP- und MAC-Adressen leer bleiben, wird die automatische Erkennung verwendet", "title": "Xiaomi Aqara Gateway" } } diff --git a/homeassistant/components/xiaomi_aqara/translations/tr.json b/homeassistant/components/xiaomi_aqara/translations/tr.json index 10d1374187e..24da29417d1 100644 --- a/homeassistant/components/xiaomi_aqara/translations/tr.json +++ b/homeassistant/components/xiaomi_aqara/translations/tr.json @@ -1,7 +1,38 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "not_xiaomi_aqara": "Xiaomi Aqara A\u011f Ge\u00e7idi de\u011fil, ke\u015ffedilen cihaz bilinen a\u011f ge\u00e7itleriyle e\u015fle\u015fmedi" + }, "error": { + "discovery_error": "Bir Xiaomi Aqara A\u011f Ge\u00e7idi ke\u015ffedilemedi, HomeAssistant'\u0131 aray\u00fcz olarak \u00e7al\u0131\u015ft\u0131ran cihaz\u0131n IP'sini kullanmay\u0131 deneyin", + "invalid_interface": "Ge\u00e7ersiz a\u011f aray\u00fcz\u00fc", + "invalid_key": "Ge\u00e7ersiz a\u011f ge\u00e7idi anahtar\u0131", "invalid_mac": "Ge\u00e7ersiz Mac Adresi" + }, + "flow_title": "Xiaomi Aqara A\u011f Ge\u00e7idi: {name}", + "step": { + "select": { + "data": { + "select_ip": "\u0130p Adresi" + }, + "description": "Ek a\u011f ge\u00e7itlerini ba\u011flamak istiyorsan\u0131z kurulumu tekrar \u00e7al\u0131\u015ft\u0131r\u0131n.", + "title": "Ba\u011flamak istedi\u011finiz Xiaomi Aqara A\u011f Ge\u00e7idini se\u00e7in" + }, + "settings": { + "data": { + "key": "A\u011f ge\u00e7idinizin anahtar\u0131", + "name": "A\u011f Ge\u00e7idinin Ad\u0131" + }, + "description": "Anahtar (parola) bu \u00f6\u011fretici kullan\u0131larak al\u0131nabilir: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. Anahtar sa\u011flanmazsa, yaln\u0131zca sens\u00f6rlere eri\u015filebilir" + }, + "user": { + "data": { + "host": "\u0130p Adresi (iste\u011fe ba\u011fl\u0131)", + "mac": "Mac Adresi (iste\u011fe ba\u011fl\u0131)" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/xiaomi_aqara/translations/uk.json b/homeassistant/components/xiaomi_aqara/translations/uk.json new file mode 100644 index 00000000000..1598e96b38e --- /dev/null +++ b/homeassistant/components/xiaomi_aqara/translations/uk.json @@ -0,0 +1,43 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454.", + "not_xiaomi_aqara": "\u0426\u0435 \u043d\u0435 \u0448\u043b\u044e\u0437 Xiaomi Aqara. \u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0432\u0456\u0434\u043f\u043e\u0432\u0456\u0434\u0430\u0454 \u0432\u0456\u0434\u043e\u043c\u0438\u043c \u0448\u043b\u044e\u0437\u0456\u0432." + }, + "error": { + "discovery_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u0432\u0438\u044f\u0432\u0438\u0442\u0438 \u0448\u043b\u044e\u0437 Xiaomi Aqara, \u0441\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u0430\u0442\u0438 IP-\u0430\u0434\u0440\u0435\u0441\u0443 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e \u0437 HomeAssistant \u0432 \u044f\u043a\u043e\u0441\u0442\u0456 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0443.", + "invalid_host": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0435 \u0434\u043e\u043c\u0435\u043d\u043d\u0435 \u0456\u043c'\u044f \u0430\u0431\u043e IP-\u0430\u0434\u0440\u0435\u0441\u0430. . \u0421\u043f\u043e\u0441\u043e\u0431\u0438 \u0432\u0438\u0440\u0456\u0448\u0435\u043d\u043d\u044f \u0446\u0456\u0454\u0457 \u043f\u0440\u043e\u0431\u043b\u0435\u043c\u0438 \u043e\u043f\u0438\u0441\u0430\u043d\u0456 \u0442\u0443\u0442: https://www.home-assistant.io/integrations/xiaomi_aqara/#connection-problem.", + "invalid_interface": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439 \u043c\u0435\u0440\u0435\u0436\u0435\u0432\u0438\u0439 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441.", + "invalid_key": "\u041d\u0435\u0432\u0456\u0440\u043d\u0438\u0439 \u043a\u043b\u044e\u0447 \u0448\u043b\u044e\u0437\u0443.", + "invalid_mac": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0430 MAC-\u0430\u0434\u0440\u0435\u0441\u0430." + }, + "flow_title": "\u0428\u043b\u044e\u0437 Xiaomi Aqara: {name}", + "step": { + "select": { + "data": { + "select_ip": "IP-\u0430\u0434\u0440\u0435\u0441\u0430" + }, + "description": "\u041f\u043e\u0447\u043d\u0456\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u044e\u0432\u0430\u043d\u043d\u044f \u0437\u043d\u043e\u0432\u0443, \u044f\u043a\u0449\u043e \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e \u0434\u043e\u0434\u0430\u0442\u0438 \u0456\u043d\u0448\u0438\u0439 \u0448\u043b\u044e\u0437", + "title": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0448\u043b\u044e\u0437 Xiaomi Aqara" + }, + "settings": { + "data": { + "key": "\u041a\u043b\u044e\u0447", + "name": "\u041d\u0430\u0437\u0432\u0430" + }, + "description": "\u041a\u043b\u044e\u0447 (\u043f\u0430\u0440\u043e\u043b\u044c) \u043c\u043e\u0436\u043d\u0430 \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0437\u0430 \u0434\u043e\u043f\u043e\u043c\u043e\u0433\u043e\u044e \u0446\u0456\u0454\u0457 \u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u0457: https://www.domoticz.com/wiki/Xiaomi_Gateway_(Aqara)#Adding_the_Xiaomi_Gateway_to_Domoticz. \u042f\u043a\u0449\u043e \u043a\u043b\u044e\u0447 \u043d\u0435 \u0432\u043a\u0430\u0437\u0430\u043d\u043e, \u0431\u0443\u0434\u0443\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0456 \u0442\u0456\u043b\u044c\u043a\u0438 \u0434\u0430\u0442\u0447\u0438\u043a\u0438.", + "title": "\u0428\u043b\u044e\u0437 Xiaomi Aqara" + }, + "user": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430 (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)", + "interface": "\u041c\u0435\u0440\u0435\u0436\u0435\u0432\u0438\u0439 \u0456\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441", + "mac": "MAC-\u0430\u0434\u0440\u0435\u0441\u0430 (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)" + }, + "description": "\u041d\u0430\u043b\u0430\u0448\u0442\u0443\u0439\u0442\u0435 Home Assistant \u0434\u043b\u044f \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 \u0437\u0456 \u0448\u043b\u044e\u0437\u043e\u043c Xiaomi Aqara. \u0414\u043b\u044f \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e\u0433\u043e \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u043d\u044f \u0448\u043b\u044e\u0437\u0443, \u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u043b\u044f IP \u0456 MAC-\u0430\u0434\u0440\u0435\u0441\u0438 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c\u0438.", + "title": "\u0428\u043b\u044e\u0437 Xiaomi Aqara" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/de.json b/homeassistant/components/xiaomi_miio/translations/de.json index 0b5f593ffcd..d56a81e14d4 100644 --- a/homeassistant/components/xiaomi_miio/translations/de.json +++ b/homeassistant/components/xiaomi_miio/translations/de.json @@ -2,27 +2,28 @@ "config": { "abort": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", - "already_in_progress": "Der Konfigurationsablauf f\u00fcr dieses Xiaomi Miio-Ger\u00e4t wird bereits ausgef\u00fchrt." + "already_in_progress": "Der Konfigurationsablauf wird bereits ausgef\u00fchrt" }, "error": { - "no_device_selected": "Kein Ger\u00e4t ausgew\u00e4hlt, bitte w\u00e4hlen Sie ein Ger\u00e4t aus." + "cannot_connect": "Verbindung fehlgeschlagen", + "no_device_selected": "Kein Ger\u00e4t ausgew\u00e4hlt, bitte w\u00e4hle ein Ger\u00e4t aus." }, "flow_title": "Xiaomi Miio: {name}", "step": { "gateway": { "data": { - "host": "IP Adresse", + "host": "IP-Adresse", "name": "Name des Gateways", "token": "API-Token" }, - "description": "Sie ben\u00f6tigen den 32 Zeichen langen API-Token. Anweisungen finden Sie unter https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token.", - "title": "Stellen Sie eine Verbindung zu einem Xiaomi Gateway her" + "description": "Du ben\u00f6tigst den 32 Zeichen langen API-Token. Anweisungen findest du unter https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token.", + "title": "Stelle eine Verbindung zu einem Xiaomi Gateway her" }, "user": { "data": { - "gateway": "Stellen Sie eine Verbindung zu einem Xiaomi Gateway her" + "gateway": "Stelle eine Verbindung zu einem Xiaomi Gateway her" }, - "description": "W\u00e4hlen Sie aus, mit welchem Ger\u00e4t Sie eine Verbindung herstellen m\u00f6chten.", + "description": "W\u00e4hle aus, mit welchem Ger\u00e4t du eine Verbindung herstellen m\u00f6chtest.", "title": "Xiaomi Miio" } } diff --git a/homeassistant/components/xiaomi_miio/translations/tr.json b/homeassistant/components/xiaomi_miio/translations/tr.json new file mode 100644 index 00000000000..46a6493ab3a --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/tr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131", + "no_device_selected": "Cihaz se\u00e7ilmedi, l\u00fctfen bir cihaz se\u00e7in." + }, + "step": { + "gateway": { + "data": { + "host": "\u0130p Adresi", + "name": "A\u011f Ge\u00e7idinin Ad\u0131", + "token": "API Belirteci" + }, + "title": "Bir Xiaomi A\u011f Ge\u00e7idine ba\u011flan\u0131n" + }, + "user": { + "data": { + "gateway": "Bir Xiaomi A\u011f Ge\u00e7idine ba\u011flan\u0131n" + }, + "description": "Hangi cihaza ba\u011flanmak istedi\u011finizi se\u00e7in.", + "title": "Xiaomi Miio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/uk.json b/homeassistant/components/xiaomi_miio/translations/uk.json new file mode 100644 index 00000000000..f32105589f6 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/uk.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u0432\u0436\u0435 \u0442\u0440\u0438\u0432\u0430\u0454." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "no_device_selected": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043e\u0434\u0438\u043d \u0437 \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457\u0432." + }, + "flow_title": "Xiaomi Miio: {name}", + "step": { + "gateway": { + "data": { + "host": "IP-\u0430\u0434\u0440\u0435\u0441\u0430", + "name": "\u041d\u0430\u0437\u0432\u0430", + "token": "\u0422\u043e\u043a\u0435\u043d API" + }, + "description": "\u0414\u043b\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u043f\u043e\u0442\u0440\u0456\u0431\u043d\u043e 32-\u0445 \u0437\u043d\u0430\u0447\u043d\u0438\u0439 \u0422\u043e\u043a\u0435\u043d API . \u041f\u0440\u043e \u0442\u0435, \u044f\u043a \u043e\u0442\u0440\u0438\u043c\u0430\u0442\u0438 \u0442\u043e\u043a\u0435\u043d, \u0412\u0438 \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0456\u0437\u043d\u0430\u0442\u0438\u0441\u044f \u0442\u0443\u0442:\nhttps://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token.\n\u0417\u0432\u0435\u0440\u043d\u0456\u0442\u044c \u0443\u0432\u0430\u0433\u0443, \u0449\u043e \u0446\u0435\u0439 \u0442\u043e\u043a\u0435\u043d \u0432\u0456\u0434\u0440\u0456\u0437\u043d\u044f\u0454\u0442\u044c\u0441\u044f \u0432\u0456\u0434 \u043a\u043b\u044e\u0447\u0430, \u044f\u043a\u0438\u0439 \u0432\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u043f\u0440\u0438 \u0456\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0456\u0457 Xiaomi Aqara.", + "title": "\u041f\u0456\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u044f \u0434\u043e \u0448\u043b\u044e\u0437\u0443 Xiaomi" + }, + "user": { + "data": { + "gateway": "\u0428\u043b\u044e\u0437 Xiaomi" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439, \u044f\u043a\u0438\u0439 \u0412\u0438 \u0445\u043e\u0447\u0435\u0442\u0435 \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438.", + "title": "Xiaomi Miio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xmpp/manifest.json b/homeassistant/components/xmpp/manifest.json index 2453ce61b05..b56d43b9c9c 100644 --- a/homeassistant/components/xmpp/manifest.json +++ b/homeassistant/components/xmpp/manifest.json @@ -2,6 +2,6 @@ "domain": "xmpp", "name": "Jabber (XMPP)", "documentation": "https://www.home-assistant.io/integrations/xmpp", - "requirements": ["slixmpp==1.5.2"], + "requirements": ["slixmpp==1.6.0"], "codeowners": ["@fabaff", "@flowolf"] } diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index 324999c7124..b61df3f810b 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -49,10 +49,12 @@ DATA_CUSTOM_EFFECTS = "custom_effects" DATA_SCAN_INTERVAL = "scan_interval" DATA_DEVICE = "device" DATA_UNSUB_UPDATE_LISTENER = "unsub_update_listener" +DATA_REMOVE_INIT_DISPATCHER = "remove_init_dispatcher" ATTR_COUNT = "count" ATTR_ACTION = "action" ATTR_TRANSITIONS = "transitions" +ATTR_MODE_MUSIC = "music_mode" ACTION_RECOVER = "recover" ACTION_STAY = "stay" @@ -181,11 +183,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Yeelight from a config entry.""" async def _initialize(host: str, capabilities: Optional[dict] = None) -> None: - async_dispatcher_connect( + remove_dispatcher = async_dispatcher_connect( hass, DEVICE_INITIALIZED.format(host), _load_platforms, ) + hass.data[DOMAIN][DATA_CONFIG_ENTRIES][entry.entry_id][ + DATA_REMOVE_INIT_DISPATCHER + ] = remove_dispatcher device = await _async_get_device(hass, host, entry, capabilities) hass.data[DOMAIN][DATA_CONFIG_ENTRIES][entry.entry_id][DATA_DEVICE] = device @@ -250,6 +255,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): if unload_ok: data = hass.data[DOMAIN][DATA_CONFIG_ENTRIES].pop(entry.entry_id) + remove_init_dispatcher = data.get(DATA_REMOVE_INIT_DISPATCHER) + if remove_init_dispatcher is not None: + remove_init_dispatcher() data[DATA_UNSUB_UPDATE_LISTENER]() data[DATA_DEVICE].async_unload() if entry.data[CONF_ID]: diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 98b7f097636..c256cfb23e0 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -49,6 +49,7 @@ from . import ( ACTION_RECOVER, ATTR_ACTION, ATTR_COUNT, + ATTR_MODE_MUSIC, ATTR_TRANSITIONS, CONF_FLOW_PARAMS, CONF_MODE_MUSIC, @@ -77,6 +78,7 @@ SUPPORT_YEELIGHT_RGB = SUPPORT_YEELIGHT_WHITE_TEMP | SUPPORT_COLOR ATTR_MINUTES = "minutes" SERVICE_SET_MODE = "set_mode" +SERVICE_SET_MUSIC_MODE = "set_music_mode" SERVICE_START_FLOW = "start_flow" SERVICE_SET_COLOR_SCENE = "set_color_scene" SERVICE_SET_HSV_SCENE = "set_hsv_scene" @@ -175,6 +177,10 @@ SERVICE_SCHEMA_SET_MODE = { vol.Required(ATTR_MODE): vol.In([mode.name.lower() for mode in PowerMode]) } +SERVICE_SCHEMA_SET_MUSIC_MODE = { + vol.Required(ATTR_MODE_MUSIC): cv.boolean, +} + SERVICE_SCHEMA_START_FLOW = YEELIGHT_FLOW_TRANSITION_SCHEMA SERVICE_SCHEMA_SET_COLOR_SCENE = { @@ -404,6 +410,11 @@ def _async_setup_services(hass: HomeAssistant): SERVICE_SCHEMA_SET_AUTO_DELAY_OFF_SCENE, _async_set_auto_delay_off_scene, ) + platform.async_register_entity_service( + SERVICE_SET_MUSIC_MODE, + SERVICE_SCHEMA_SET_MUSIC_MODE, + "set_music_mode", + ) class YeelightGenericLight(YeelightEntity, LightEntity): @@ -415,7 +426,6 @@ class YeelightGenericLight(YeelightEntity, LightEntity): self.config = device.config - self._brightness = None self._color_temp = None self._hs = None self._effect = None @@ -476,10 +486,14 @@ class YeelightGenericLight(YeelightEntity, LightEntity): @property def brightness(self) -> int: """Return the brightness of this light between 1..255.""" - temp = self._get_property(self._brightness_property) - if temp: - self._brightness = temp - return round(255 * (int(self._brightness) / 100)) + # Always use "bright" as property name in music mode + # Since music mode states are only caches in upstream library + # and the cache key is always "bright" for brightness + brightness_property = ( + "bright" if self._bulb.music_mode else self._brightness_property + ) + brightness = self._get_property(brightness_property) + return round(255 * (int(brightness) / 100)) @property def min_mireds(self): @@ -550,7 +564,11 @@ class YeelightGenericLight(YeelightEntity, LightEntity): def device_state_attributes(self): """Return the device specific state attributes.""" - attributes = {"flowing": self.device.is_color_flow_enabled} + attributes = { + "flowing": self.device.is_color_flow_enabled, + "music_mode": self._bulb.music_mode, + } + if self.device.is_nightlight_supported: attributes["night_light"] = self.device.is_nightlight_enabled @@ -591,13 +609,18 @@ class YeelightGenericLight(YeelightEntity, LightEntity): return color_util.color_RGB_to_hs(red, green, blue) - def set_music_mode(self, mode) -> None: + def set_music_mode(self, music_mode) -> None: """Set the music mode on or off.""" - if mode: - self._bulb.start_music() + if music_mode: + try: + self._bulb.start_music() + except AssertionError as ex: + _LOGGER.error(ex) else: self._bulb.stop_music() + self.device.update() + @_cmd def set_brightness(self, brightness, duration) -> None: """Set bulb brightness.""" diff --git a/homeassistant/components/yeelight/services.yaml b/homeassistant/components/yeelight/services.yaml index 5e7f2419f16..b519d0c91d9 100644 --- a/homeassistant/components/yeelight/services.yaml +++ b/homeassistant/components/yeelight/services.yaml @@ -85,3 +85,12 @@ start_flow: transitions: description: Array of transitions, for desired effect. Examples https://yeelight.readthedocs.io/en/stable/flow.html example: '[{ "TemperatureTransition": [1900, 1000, 80] }, { "TemperatureTransition": [1900, 1000, 10] }]' +set_music_mode: + description: Enable or disable music_mode + fields: + entity_id: + description: Name of the light entity. + example: "light.yeelight" + music_mode: + description: Use true or false to enable / disable music_mode + example: true diff --git a/homeassistant/components/yeelight/translations/de.json b/homeassistant/components/yeelight/translations/de.json index 90c154f882b..6eaff2e87a3 100644 --- a/homeassistant/components/yeelight/translations/de.json +++ b/homeassistant/components/yeelight/translations/de.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert", + "no_devices_found": "Keine Ger\u00e4te im Netzwerk gefunden" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen" + }, "step": { "pick_device": { "data": { @@ -9,7 +16,8 @@ "user": { "data": { "host": "Host" - } + }, + "description": "Wenn du den Host leer l\u00e4sst, wird die Erkennung verwendet, um Ger\u00e4te zu finden." } } }, diff --git a/homeassistant/components/yeelight/translations/tr.json b/homeassistant/components/yeelight/translations/tr.json new file mode 100644 index 00000000000..322f13f47b0 --- /dev/null +++ b/homeassistant/components/yeelight/translations/tr.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f" + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "pick_device": { + "data": { + "device": "Cihaz" + } + }, + "user": { + "data": { + "host": "Ana Bilgisayar" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "model": "Model (Opsiyonel)", + "save_on_change": "De\u011fi\u015fiklikte Durumu Kaydet", + "transition": "Ge\u00e7i\u015f S\u00fcresi (ms)", + "use_music_mode": "M\u00fczik Modunu Etkinle\u015ftir" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/yeelight/translations/uk.json b/homeassistant/components/yeelight/translations/uk.json new file mode 100644 index 00000000000..0a173ccb6e4 --- /dev/null +++ b/homeassistant/components/yeelight/translations/uk.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "pick_device": { + "data": { + "device": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439" + } + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442" + }, + "description": "\u042f\u043a\u0449\u043e \u043d\u0435 \u0432\u043a\u0430\u0437\u0443\u0432\u0430\u0442\u0438 \u0430\u0434\u0440\u0435\u0441\u0443 \u0445\u043e\u0441\u0442\u0430, \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u0431\u0443\u0434\u0443\u0442\u044c \u0432\u0438\u044f\u0432\u043b\u0435\u043d\u0456 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e." + } + } + }, + "options": { + "step": { + "init": { + "data": { + "model": "\u041c\u043e\u0434\u0435\u043b\u044c (\u043d\u0435\u043e\u0431\u043e\u0432'\u044f\u0437\u043a\u043e\u0432\u043e)", + "nightlight_switch": "\u041f\u0435\u0440\u0435\u043c\u0438\u043a\u0430\u0447 \u0434\u043b\u044f \u043d\u0456\u0447\u043d\u0438\u043a\u0430", + "save_on_change": "\u0417\u0431\u0435\u0440\u0456\u0433\u0430\u0442\u0438 \u0441\u0442\u0430\u0442\u0443\u0441 \u043f\u0440\u0438 \u0437\u043c\u0456\u043d\u0456", + "transition": "\u0427\u0430\u0441 \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0443 (\u0432 \u043c\u0456\u043b\u0456\u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", + "use_music_mode": "\u041c\u0443\u0437\u0438\u0447\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c" + }, + "description": "\u042f\u043a\u0449\u043e \u043c\u043e\u0434\u0435\u043b\u044c \u043d\u0435 \u0432\u0438\u0431\u0440\u0430\u043d\u043e, \u0432\u043e\u043d\u0430 \u0431\u0443\u0434\u0435 \u0432\u0438\u0437\u043d\u0430\u0447\u0435\u043d\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index fdf4b98faf8..2ef7db3a1b4 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -45,7 +45,11 @@ ATTR_TYPE = "type" ATTR_PROPERTIES = "properties" ZEROCONF_TYPE = "_home-assistant._tcp.local." -HOMEKIT_TYPE = "_hap._tcp.local." +HOMEKIT_TYPES = [ + "_hap._tcp.local.", + # Thread based devices + "_hap._udp.local.", +] CONF_DEFAULT_INTERFACE = "default_interface" CONF_IPV6 = "ipv6" @@ -229,8 +233,9 @@ async def _async_start_zeroconf_browser(hass, zeroconf): types = list(zeroconf_types) - if HOMEKIT_TYPE not in zeroconf_types: - types.append(HOMEKIT_TYPE) + for hk_type in HOMEKIT_TYPES: + if hk_type not in zeroconf_types: + types.append(hk_type) def service_update(zeroconf, service_type, name, state_change): """Service state changed.""" @@ -261,7 +266,7 @@ async def _async_start_zeroconf_browser(hass, zeroconf): _LOGGER.debug("Discovered new device %s %s", name, info) # If we can handle it as a HomeKit discovery, we do that here. - if service_type == HOMEKIT_TYPE: + if service_type in HOMEKIT_TYPES: discovery_was_forwarded = handle_homekit(hass, homekit_models, info) # Continue on here as homekit_controller # still needs to get updates on devices @@ -294,7 +299,9 @@ async def _async_start_zeroconf_browser(hass, zeroconf): else: uppercase_mac = None - for entry in zeroconf_types[service_type]: + # Not all homekit types are currently used for discovery + # so not all service type exist in zeroconf_types + for entry in zeroconf_types.get(service_type, []): if len(entry) > 1: if ( uppercase_mac is not None diff --git a/homeassistant/components/zerproc/translations/tr.json b/homeassistant/components/zerproc/translations/tr.json index 49fa9545e94..3df15466f03 100644 --- a/homeassistant/components/zerproc/translations/tr.json +++ b/homeassistant/components/zerproc/translations/tr.json @@ -1,7 +1,13 @@ { "config": { "abort": { - "no_devices_found": "A\u011fda cihaz bulunamad\u0131" + "no_devices_found": "A\u011fda cihaz bulunamad\u0131", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "step": { + "confirm": { + "description": "Kuruluma ba\u015flamak ister misiniz?" + } } } } \ No newline at end of file diff --git a/homeassistant/components/zerproc/translations/uk.json b/homeassistant/components/zerproc/translations/uk.json new file mode 100644 index 00000000000..292861e9129 --- /dev/null +++ b/homeassistant/components/zerproc/translations/uk.json @@ -0,0 +1,13 @@ +{ + "config": { + "abort": { + "no_devices_found": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0457 \u043d\u0435 \u0437\u043d\u0430\u0439\u0434\u0435\u043d\u0456 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456.", + "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." + }, + "step": { + "confirm": { + "description": "\u0425\u043e\u0447\u0435\u0442\u0435 \u043f\u043e\u0447\u0430\u0442\u0438 \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f?" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 527b218e1f3..a24c20872f2 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -7,10 +7,10 @@ "bellows==0.21.0", "pyserial==3.5", "pyserial-asyncio==0.5", - "zha-quirks==0.0.51", + "zha-quirks==0.0.53", "zigpy-cc==0.5.2", "zigpy-deconz==0.11.1", - "zigpy==0.30.0", + "zigpy==0.32.0", "zigpy-xbee==0.13.0", "zigpy-zigate==0.7.3", "zigpy-znp==0.3.0" diff --git a/homeassistant/components/zha/translations/cs.json b/homeassistant/components/zha/translations/cs.json index 1ac4c7c2d61..cedf56d73c4 100644 --- a/homeassistant/components/zha/translations/cs.json +++ b/homeassistant/components/zha/translations/cs.json @@ -51,20 +51,20 @@ "device_rotated": "Za\u0159\u00edzen\u00ed oto\u010deno \"{subtype}\"", "device_shaken": "Za\u0159\u00edzen\u00ed se zat\u0159\u00e1slo", "device_tilted": "Za\u0159\u00edzen\u00ed naklon\u011bno", - "remote_button_alt_double_press": "Dvakr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\" (alternativn\u00ed re\u017eim)", + "remote_button_alt_double_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto dvakr\u00e1t (alternativn\u00ed re\u017eim)", "remote_button_alt_long_release": "Uvoln\u011bno tla\u010d\u00edtko \"{subtype}\" po dlouh\u00e9m stisku (alternativn\u00ed re\u017eim)", - "remote_button_alt_quadruple_press": "\u010cty\u0159ikr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\" (alternativn\u00ed re\u017eim)", - "remote_button_alt_quintuple_press": "P\u011btkr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\" (alternativn\u00ed re\u017eim)", - "remote_button_alt_short_press": "Stiknuto tla\u010d\u00edtko \"{subtype}\" (alternativn\u00ed re\u017eim)", + "remote_button_alt_quadruple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto \u010dty\u0159ikr\u00e1t (alternativn\u00ed re\u017eim)", + "remote_button_alt_quintuple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto p\u011btkr\u00e1t (alternativn\u00ed re\u017eim)", + "remote_button_alt_short_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto (alternativn\u00ed re\u017eim)", "remote_button_alt_short_release": "Uvoln\u011bno tla\u010d\u00edtko \"{subtype}\" (alternativn\u00ed re\u017eim)", - "remote_button_alt_triple_press": "T\u0159ikr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\" (alternativn\u00ed re\u017eim)", - "remote_button_double_press": "Dvakr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\"", + "remote_button_alt_triple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto t\u0159ikr\u00e1t (alternativn\u00ed re\u017eim)", + "remote_button_double_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto dvakr\u00e1t", "remote_button_long_release": "Uvoln\u011bno tla\u010d\u00edtko \"{subtype}\" po dlouh\u00e9m stisku", - "remote_button_quadruple_press": "\u010cty\u0159ikr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\"", - "remote_button_quintuple_press": "P\u011btkr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\"", - "remote_button_short_press": "Stiknuto tla\u010d\u00edtko \"{subtype}\"", + "remote_button_quadruple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto \u010dty\u0159ikr\u00e1t", + "remote_button_quintuple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto p\u011btkr\u00e1t", + "remote_button_short_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto", "remote_button_short_release": "Uvoln\u011bno tla\u010d\u00edtko \"{subtype}\"", - "remote_button_triple_press": "T\u0159ikr\u00e1t stisknuto tla\u010d\u00edtko \"{subtype}\"" + "remote_button_triple_press": "Tla\u010d\u00edtko \"{subtype}\" stisknuto t\u0159ikr\u00e1t" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index 592450fcfbc..61e9b8e37ba 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "single_instance_allowed": "Es ist nur eine einzige Konfiguration von ZHA zul\u00e4ssig." + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "error": { - "cannot_connect": "Kein Verbindung zu ZHA-Ger\u00e4t m\u00f6glich" + "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { "pick_radio": { diff --git a/homeassistant/components/zha/translations/pl.json b/homeassistant/components/zha/translations/pl.json index ab4402558ba..4cdada49f50 100644 --- a/homeassistant/components/zha/translations/pl.json +++ b/homeassistant/components/zha/translations/pl.json @@ -70,22 +70,22 @@ "device_shaken": "nast\u0105pi potrz\u0105\u015bni\u0119cie urz\u0105dzeniem", "device_slid": "nast\u0105pi przesuni\u0119cie urz\u0105dzenia \"{subtype}\"", "device_tilted": "nast\u0105pi przechylenie urz\u0105dzenia", - "remote_button_alt_double_press": "\"{subtype}\" dwukrotnie naci\u015bni\u0119ty (tryb alternatywny)", - "remote_button_alt_long_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y (tryb alternatywny)", - "remote_button_alt_long_release": "\"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu (tryb alternatywny)", - "remote_button_alt_quadruple_press": "\"{subtype}\" czterokrotnie naci\u015bni\u0119ty (tryb alternatywny)", - "remote_button_alt_quintuple_press": "\"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty (tryb alternatywny)", - "remote_button_alt_short_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty (tryb alternatywny)", - "remote_button_alt_short_release": "\"{subtype}\" zostanie zwolniony (tryb alternatywny)", - "remote_button_alt_triple_press": "\"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty (tryb alternatywny)", - "remote_button_double_press": "\"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty", - "remote_button_long_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", - "remote_button_long_release": "\"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", - "remote_button_quadruple_press": "\"{subtype}\" czterokrotnie naci\u015bni\u0119ty", - "remote_button_quintuple_press": "\"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty", - "remote_button_short_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty", - "remote_button_short_release": "\"{subtype}\" zostanie zwolniony", - "remote_button_triple_press": "\"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty" + "remote_button_alt_double_press": "przycisk \"{subtype}\" zostanie dwukrotnie naci\u015bni\u0119ty (tryb alternatywny)", + "remote_button_alt_long_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y (tryb alternatywny)", + "remote_button_alt_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu (tryb alternatywny)", + "remote_button_alt_quadruple_press": "przycisk \"{subtype}\" zostanie czterokrotnie naci\u015bni\u0119ty (tryb alternatywny)", + "remote_button_alt_quintuple_press": "przycisk \"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty (tryb alternatywny)", + "remote_button_alt_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty (tryb alternatywny)", + "remote_button_alt_short_release": "przycisk \"{subtype}\" zostanie zwolniony (tryb alternatywny)", + "remote_button_alt_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty (tryb alternatywny)", + "remote_button_double_press": "przycisk \"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty", + "remote_button_long_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", + "remote_button_long_release": "przycisk \"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", + "remote_button_quadruple_press": "przycisk \"{subtype}\" czterokrotnie naci\u015bni\u0119ty", + "remote_button_quintuple_press": "przycisk \"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty", + "remote_button_short_press": "przycisk \"{subtype}\" zostanie naci\u015bni\u0119ty", + "remote_button_short_release": "przycisk \"{subtype}\" zostanie zwolniony", + "remote_button_triple_press": "przycisk \"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty" } } } \ No newline at end of file diff --git a/homeassistant/components/zha/translations/tr.json b/homeassistant/components/zha/translations/tr.json new file mode 100644 index 00000000000..a74f56a2f4e --- /dev/null +++ b/homeassistant/components/zha/translations/tr.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, + "error": { + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "step": { + "pick_radio": { + "title": "Radyo Tipi" + }, + "port_config": { + "data": { + "path": "Seri cihaz yolu" + }, + "title": "Ayarlar" + } + } + }, + "device_automation": { + "trigger_type": { + "device_offline": "Cihaz \u00e7evrimd\u0131\u015f\u0131" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zha/translations/uk.json b/homeassistant/components/zha/translations/uk.json new file mode 100644 index 00000000000..7bd62cf26e1 --- /dev/null +++ b/homeassistant/components/zha/translations/uk.json @@ -0,0 +1,91 @@ +{ + "config": { + "abort": { + "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." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f" + }, + "step": { + "pick_radio": { + "data": { + "radio_type": "\u0422\u0438\u043f \u0440\u0430\u0434\u0456\u043e\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u0442\u0438\u043f \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e Zigbee", + "title": "\u0422\u0438\u043f \u0440\u0430\u0434\u0456\u043e\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + }, + "port_config": { + "data": { + "baudrate": "\u0448\u0432\u0438\u0434\u043a\u0456\u0441\u0442\u044c \u043f\u043e\u0440\u0442\u0443", + "flow_control": "\u043a\u0435\u0440\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u043e\u0442\u043e\u043a\u043e\u043c \u0434\u0430\u043d\u0438\u0445", + "path": "\u0428\u043b\u044f\u0445 \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + }, + "description": "\u0412\u043a\u0430\u0436\u0456\u0442\u044c \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043f\u043e\u0440\u0442\u0443", + "title": "\u041f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0438" + }, + "user": { + "data": { + "path": "\u0428\u043b\u044f\u0445 \u0434\u043e \u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + }, + "description": "\u0412\u0438\u0431\u0435\u0440\u0456\u0442\u044c \u043f\u043e\u0441\u043b\u0456\u0434\u043e\u0432\u043d\u0438\u0439 \u043f\u043e\u0440\u0442 \u043a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u043e\u0440\u0430 \u043c\u0435\u0440\u0435\u0436\u0456 Zigbee", + "title": "Zigbee Home Automation" + } + } + }, + "device_automation": { + "action_type": { + "squawk": "\u0422\u0440\u0430\u043d\u0441\u043f\u043e\u043d\u0434\u0435\u0440", + "warn": "\u0423\u0432\u0456\u043c\u043a\u043d\u0435\u043d\u043d\u044f \u043e\u043f\u043e\u0432\u0456\u0449\u0435\u043d\u043d\u044f" + }, + "trigger_subtype": { + "both_buttons": "\u041e\u0431\u0438\u0434\u0432\u0456 \u043a\u043d\u043e\u043f\u043a\u0438", + "button_1": "\u041f\u0435\u0440\u0448\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "button_2": "\u0414\u0440\u0443\u0433\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "button_3": "\u0422\u0440\u0435\u0442\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "button_5": "\u041f'\u044f\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "button_6": "\u0428\u043e\u0441\u0442\u0430 \u043a\u043d\u043e\u043f\u043a\u0430", + "close": "\u0417\u0430\u043a\u0440\u0438\u0432\u0430\u0454\u0442\u044c\u0441\u044f", + "dim_down": "\u0417\u043c\u0435\u043d\u0448\u0438\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c", + "dim_up": "\u0417\u0431\u0456\u043b\u044c\u0448\u0438\u0442\u0438 \u044f\u0441\u043a\u0440\u0430\u0432\u0456\u0441\u0442\u044c", + "face_1": "\u041d\u0430 \u043f\u0435\u0440\u0448\u0456\u0439 \u0433\u0440\u0430\u043d\u0456", + "face_2": "\u041d\u0430 \u0434\u0440\u0443\u0433\u0438\u0439 \u0433\u0440\u0430\u043d\u0456", + "face_3": "\u041d\u0430 \u0442\u0440\u0435\u0442\u0456\u0439 \u0433\u0440\u0430\u043d\u0456", + "face_4": "\u041d\u0430 \u0447\u0435\u0442\u0432\u0435\u0440\u0442\u0456\u0439 \u0433\u0440\u0430\u043d\u0456", + "face_5": "\u041d\u0430 \u043f'\u044f\u0442\u0456\u0439 \u0433\u0440\u0430\u043d\u0456", + "face_6": "\u041d\u0430 \u0448\u043e\u0441\u0442\u0438\u0439 \u0433\u0440\u0430\u043d\u0456", + "face_any": "\u041d\u0430 \u0431\u0443\u0434\u044c-\u044f\u043a\u0456\u0439 \u0433\u0440\u0430\u043d\u0456", + "left": "\u041b\u0456\u0432\u043e\u0440\u0443\u0447", + "open": "\u0412\u0456\u0434\u043a\u0440\u0438\u0432\u0430\u0454\u0442\u044c\u0441\u044f", + "right": "\u041f\u0440\u0430\u0432\u043e\u0440\u0443\u0447", + "turn_off": "\u0412\u0438\u043c\u043a\u043d\u0443\u0442\u0438", + "turn_on": "\u0423\u0432\u0456\u043c\u043a\u043d\u0443\u0442\u0438" + }, + "trigger_type": { + "device_dropped": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0441\u043a\u0438\u043d\u0443\u043b\u0438", + "device_flipped": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u0435\u0440\u0435\u0432\u0435\u0440\u043d\u0443\u043b\u0438 {subtype}", + "device_knocked": "\u041f\u0440\u0438\u0441\u0442\u0440\u043e\u0454\u043c \u043f\u043e\u0441\u0442\u0443\u043a\u0430\u043b\u0438 {subtype}", + "device_offline": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0435 \u0432 \u043c\u0435\u0440\u0435\u0436\u0456", + "device_rotated": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u043e\u0432\u0435\u0440\u043d\u0443\u043b\u0438 {subtype}", + "device_shaken": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043f\u043e\u0442\u0440\u044f\u0441\u043b\u0438", + "device_slid": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0437\u0440\u0443\u0448\u0438\u043b\u0438 {subtype}", + "device_tilted": "\u041f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u043d\u0430\u0445\u0438\u043b\u0438\u043b\u0438", + "remote_button_alt_double_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0438 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_long_press": "{subtype} \u0434\u043e\u0432\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_long_release": "{subtype} \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0456\u0441\u043b\u044f \u0434\u043e\u0432\u0433\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_quadruple_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0447\u043e\u0442\u0438\u0440\u0438 \u0440\u0430\u0437\u0438 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_quintuple_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u043f'\u044f\u0442\u044c \u0440\u0430\u0437\u0456\u0432 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_short_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_short_release": "{subtype} \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0456\u0441\u043b\u044f \u0434\u043e\u0432\u0433\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_alt_triple_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0438 (\u0430\u043b\u044c\u0442\u0435\u0440\u043d\u0430\u0442\u0438\u0432\u043d\u0438\u0439 \u0440\u0435\u0436\u0438\u043c)", + "remote_button_double_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0434\u0432\u0430 \u0440\u0430\u0437\u0438", + "remote_button_long_press": "{subtype} \u0434\u043e\u0432\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430", + "remote_button_long_release": "{subtype} \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0456\u0441\u043b\u044f \u0434\u043e\u0432\u0433\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f", + "remote_button_quadruple_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0447\u043e\u0442\u0438\u0440\u0438 \u0440\u0430\u0437\u0438", + "remote_button_quintuple_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u043f'\u044f\u0442\u044c \u0440\u0430\u0437\u0456\u0432", + "remote_button_short_press": "\u041a\u043d\u043e\u043f\u043a\u0430 \"{subtype}\" \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430", + "remote_button_short_release": "{subtype} \u0432\u0456\u0434\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u0456\u0441\u043b\u044f \u043a\u043e\u0440\u043e\u0442\u043a\u043e\u0433\u043e \u043d\u0430\u0442\u0438\u0441\u043a\u0430\u043d\u043d\u044f", + "remote_button_triple_press": "{subtype} \u043d\u0430\u0442\u0438\u0441\u043d\u0443\u0442\u0430 \u0442\u0440\u0438 \u0440\u0430\u0437\u0438" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zodiac/translations/sensor.tr.json b/homeassistant/components/zodiac/translations/sensor.tr.json new file mode 100644 index 00000000000..f9e0357799d --- /dev/null +++ b/homeassistant/components/zodiac/translations/sensor.tr.json @@ -0,0 +1,18 @@ +{ + "state": { + "zodiac__sign": { + "aquarius": "Kova", + "aries": "Ko\u00e7", + "cancer": "Yenge\u00e7", + "capricorn": "O\u011flak", + "gemini": "Ikizler", + "leo": "Aslan", + "libra": "Terazi", + "pisces": "Bal\u0131k", + "sagittarius": "Yay", + "scorpio": "Akrep", + "taurus": "Bo\u011fa", + "virgo": "Ba\u015fak" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zodiac/translations/sensor.uk.json b/homeassistant/components/zodiac/translations/sensor.uk.json new file mode 100644 index 00000000000..e0c891a8b23 --- /dev/null +++ b/homeassistant/components/zodiac/translations/sensor.uk.json @@ -0,0 +1,18 @@ +{ + "state": { + "zodiac__sign": { + "aquarius": "\u0412\u043e\u0434\u043e\u043b\u0456\u0439", + "aries": "\u041e\u0432\u0435\u043d", + "cancer": "\u0420\u0430\u043a", + "capricorn": "\u041a\u043e\u0437\u0435\u0440\u0456\u0433", + "gemini": "\u0411\u043b\u0438\u0437\u043d\u044e\u043a\u0438", + "leo": "\u041b\u0435\u0432", + "libra": "\u0422\u0435\u0440\u0435\u0437\u0438", + "pisces": "\u0420\u0438\u0431\u0438", + "sagittarius": "\u0421\u0442\u0440\u0456\u043b\u0435\u0446\u044c", + "scorpio": "\u0421\u043a\u043e\u0440\u043f\u0456\u043e\u043d", + "taurus": "\u0422\u0435\u043b\u0435\u0446\u044c", + "virgo": "\u0414\u0456\u0432\u0430" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zone/translations/tr.json b/homeassistant/components/zone/translations/tr.json new file mode 100644 index 00000000000..dad65ac92a7 --- /dev/null +++ b/homeassistant/components/zone/translations/tr.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "init": { + "data": { + "latitude": "Enlem", + "longitude": "Boylam" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/de.json b/homeassistant/components/zoneminder/translations/de.json index 1362dcbd62d..5fa5d0a5234 100644 --- a/homeassistant/components/zoneminder/translations/de.json +++ b/homeassistant/components/zoneminder/translations/de.json @@ -1,10 +1,19 @@ { "config": { + "abort": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "invalid_auth": "Ung\u00fcltige Authentifizierung" + }, "flow_title": "ZoneMinder", "step": { "user": { "data": { "password": "Passwort", + "ssl": "Nutzt ein SSL-Zertifikat", "username": "Benutzername", "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" } diff --git a/homeassistant/components/zoneminder/translations/tr.json b/homeassistant/components/zoneminder/translations/tr.json new file mode 100644 index 00000000000..971f8cc9bd7 --- /dev/null +++ b/homeassistant/components/zoneminder/translations/tr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "auth_fail": "Kullan\u0131c\u0131 ad\u0131 veya \u015fifre yanl\u0131\u015f.", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "error": { + "auth_fail": "Kullan\u0131c\u0131 ad\u0131 veya \u015fifre yanl\u0131\u015f.", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_auth": "Ge\u00e7ersiz kimlik do\u011frulama" + }, + "step": { + "user": { + "data": { + "password": "Parola", + "username": "Kullan\u0131c\u0131 Ad\u0131" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zoneminder/translations/uk.json b/homeassistant/components/zoneminder/translations/uk.json new file mode 100644 index 00000000000..e5b04ae124f --- /dev/null +++ b/homeassistant/components/zoneminder/translations/uk.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "auth_fail": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439 \u043b\u043e\u0433\u0456\u043d \u0430\u0431\u043e \u043f\u0430\u0440\u043e\u043b\u044c.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "connection_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f \u0434\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 ZoneMinder.", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "create_entry": { + "default": "\u0414\u043e\u0434\u0430\u043d\u043e \u0441\u0435\u0440\u0432\u0435\u0440 ZoneMinder." + }, + "error": { + "auth_fail": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u0438\u0439 \u043b\u043e\u0433\u0456\u043d \u0430\u0431\u043e \u043f\u0430\u0440\u043e\u043b\u044c.", + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "connection_error": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u0438\u0441\u044f \u0434\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 ZoneMinder.", + "invalid_auth": "\u041d\u0435\u0432\u0456\u0440\u043d\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0456\u043a\u0430\u0446\u0456\u044f." + }, + "flow_title": "ZoneMinder", + "step": { + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442 \u0456 \u043f\u043e\u0440\u0442 (\u043d\u0430\u043f\u0440\u0438\u043a\u043b\u0430\u0434: 10.10.0.4:8010)", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "path": "\u0428\u043b\u044f\u0445 \u0434\u043e ZM", + "path_zms": "\u0428\u043b\u044f\u0445 \u0434\u043e ZMS", + "ssl": "\u0412\u0438\u043a\u043e\u0440\u0438\u0441\u0442\u043e\u0432\u0443\u0454\u0442\u044c\u0441\u044f \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442 SSL", + "username": "\u0406\u043c'\u044f \u043a\u043e\u0440\u0438\u0441\u0442\u0443\u0432\u0430\u0447\u0430", + "verify_ssl": "\u041f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0430 \u0441\u0435\u0440\u0442\u0438\u0444\u0456\u043a\u0430\u0442\u0430 SSL" + }, + "title": "ZoneMinder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index d679de7cfbd..27f6c0a4801 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -28,6 +28,7 @@ from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_component import DEFAULT_SCAN_INTERVAL from homeassistant.helpers.entity_platform import EntityPlatform from homeassistant.helpers.entity_registry import ( + async_entries_for_config_entry, async_get_registry as async_get_entity_registry, ) from homeassistant.helpers.entity_values import EntityValues @@ -81,6 +82,8 @@ CONF_DEVICE_CONFIG = "device_config" CONF_DEVICE_CONFIG_GLOB = "device_config_glob" CONF_DEVICE_CONFIG_DOMAIN = "device_config_domain" +DATA_ZWAVE_CONFIG_YAML_PRESENT = "zwave_config_yaml_present" + DEFAULT_CONF_IGNORED = False DEFAULT_CONF_INVERT_OPENCLOSE_BUTTONS = False DEFAULT_CONF_INVERT_PERCENT = False @@ -250,6 +253,64 @@ CONFIG_SCHEMA = vol.Schema( ) +async def async_get_ozw_migration_data(hass): + """Return dict with info for migration to ozw integration.""" + data_to_migrate = {} + + zwave_config_entries = hass.config_entries.async_entries(DOMAIN) + if not zwave_config_entries: + _LOGGER.error("Config entry not set up") + return data_to_migrate + + if hass.data.get(DATA_ZWAVE_CONFIG_YAML_PRESENT): + _LOGGER.warning( + "Remove %s from configuration.yaml " + "to avoid setting up this integration on restart " + "after completing migration to ozw", + DOMAIN, + ) + + config_entry = zwave_config_entries[0] # zwave only has a single config entry + ent_reg = await async_get_entity_registry(hass) + entity_entries = async_entries_for_config_entry(ent_reg, config_entry.entry_id) + unique_entries = {entry.unique_id: entry for entry in entity_entries} + dev_reg = await async_get_device_registry(hass) + + for entity_values in hass.data[DATA_ENTITY_VALUES]: + node = entity_values.primary.node + unique_id = compute_value_unique_id(node, entity_values.primary) + if unique_id not in unique_entries: + continue + device_identifier, _ = node_device_id_and_name( + node, entity_values.primary.instance + ) + device_entry = dev_reg.async_get_device({device_identifier}, set()) + data_to_migrate[unique_id] = { + "node_id": node.node_id, + "node_instance": entity_values.primary.instance, + "device_id": device_entry.id, + "command_class": entity_values.primary.command_class, + "command_class_label": entity_values.primary.label, + "value_index": entity_values.primary.index, + "unique_id": unique_id, + "entity_entry": unique_entries[unique_id], + } + + return data_to_migrate + + +@callback +def async_is_ozw_migrated(hass): + """Return True if migration to ozw is done.""" + ozw_config_entries = hass.config_entries.async_entries("ozw") + if not ozw_config_entries: + return False + + ozw_config_entry = ozw_config_entries[0] # only one ozw entry is allowed + migrated = bool(ozw_config_entry.data.get("migrated")) + return migrated + + def _obj_to_dict(obj): """Convert an object into a hash for debug.""" return { @@ -312,6 +373,7 @@ async def async_setup(hass, config): conf = config[DOMAIN] hass.data[DATA_ZWAVE_CONFIG] = conf + hass.data[DATA_ZWAVE_CONFIG_YAML_PRESENT] = True if not hass.config_entries.async_entries(DOMAIN): hass.async_create_task( @@ -343,6 +405,12 @@ async def async_setup_entry(hass, config_entry): # pylint: enable=import-error from pydispatch import dispatcher + if async_is_ozw_migrated(hass): + _LOGGER.error( + "Migration to ozw has been done. Please remove the zwave integration" + ) + return False + # Merge config entry and yaml config config = config_entry.data if DATA_ZWAVE_CONFIG in hass.data: @@ -505,7 +573,7 @@ async def async_setup_entry(hass, config_entry): async def _remove_device(node): dev_reg = await async_get_device_registry(hass) identifier, name = node_device_id_and_name(node) - device = dev_reg.async_get_device(identifiers={identifier}, connections=set()) + device = dev_reg.async_get_device(identifiers={identifier}) if device is not None: _LOGGER.debug("Removing Device - %s - %s", device.id, name) dev_reg.async_remove_device(device.id) diff --git a/homeassistant/components/zwave/manifest.json b/homeassistant/components/zwave/manifest.json index 5fda2eac7c3..6623036d2fe 100644 --- a/homeassistant/components/zwave/manifest.json +++ b/homeassistant/components/zwave/manifest.json @@ -1,8 +1,9 @@ { "domain": "zwave", - "name": "Z-Wave", + "name": "Z-Wave (deprecated)", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave", "requirements": ["homeassistant-pyozw==0.1.10", "pydispatcher==2.0.5"], + "after_dependencies": ["ozw"], "codeowners": ["@home-assistant/z-wave"] } diff --git a/homeassistant/components/zwave/node_entity.py b/homeassistant/components/zwave/node_entity.py index 064f21c69b4..56dea1639a3 100644 --- a/homeassistant/components/zwave/node_entity.py +++ b/homeassistant/components/zwave/node_entity.py @@ -246,14 +246,12 @@ class ZWaveNodeEntity(ZWaveBaseEntity): # Set the name in the devices. If they're customised # the customisation will not be stored as name and will stick. dev_reg = await get_dev_reg(self.hass) - device = dev_reg.async_get_device(identifiers={identifier}, connections=set()) + device = dev_reg.async_get_device(identifiers={identifier}) dev_reg.async_update_device(device.id, name=self._name) # update sub-devices too for i in count(2): identifier, new_name = node_device_id_and_name(self.node, i) - device = dev_reg.async_get_device( - identifiers={identifier}, connections=set() - ) + device = dev_reg.async_get_device(identifiers={identifier}) if not device: break dev_reg.async_update_device(device.id, name=new_name) diff --git a/homeassistant/components/zwave/strings.json b/homeassistant/components/zwave/strings.json index 852b8ca22fa..69401b171e2 100644 --- a/homeassistant/components/zwave/strings.json +++ b/homeassistant/components/zwave/strings.json @@ -2,8 +2,7 @@ "config": { "step": { "user": { - "title": "Set up Z-Wave", - "description": "See https://www.home-assistant.io/docs/z-wave/installation/ for information on the configuration variables", + "description": "This integration is no longer maintained. For new installations, use Z-Wave JS instead.\n\nSee https://www.home-assistant.io/docs/z-wave/installation/ for information on the configuration variables", "data": { "usb_path": "[%key:common::config_flow::data::usb_path%]", "network_key": "Network Key (leave blank to auto-generate)" diff --git a/homeassistant/components/zwave/translations/de.json b/homeassistant/components/zwave/translations/de.json index 60b5aa88024..f592c2243ac 100644 --- a/homeassistant/components/zwave/translations/de.json +++ b/homeassistant/components/zwave/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Z-Wave ist bereits konfiguriert" + "already_configured": "Z-Wave ist bereits konfiguriert", + "single_instance_allowed": "Bereits konfiguriert. Nur eine einzige Konfiguration m\u00f6glich." }, "error": { "option_error": "Z-Wave-Validierung fehlgeschlagen. Ist der Pfad zum USB-Stick korrekt?" diff --git a/homeassistant/components/zwave/translations/tr.json b/homeassistant/components/zwave/translations/tr.json index 3938868d280..383ccc6cc4f 100644 --- a/homeassistant/components/zwave/translations/tr.json +++ b/homeassistant/components/zwave/translations/tr.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "single_instance_allowed": "Zaten yap\u0131land\u0131r\u0131lm\u0131\u015f. Yaln\u0131zca tek bir konfig\u00fcrasyon m\u00fcmk\u00fcnd\u00fcr." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/zwave/translations/uk.json b/homeassistant/components/zwave/translations/uk.json index d00986cae58..5cdd6060cc4 100644 --- a/homeassistant/components/zwave/translations/uk.json +++ b/homeassistant/components/zwave/translations/uk.json @@ -1,14 +1,33 @@ { + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant.", + "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." + }, + "error": { + "option_error": "\u041f\u043e\u043c\u0438\u043b\u043a\u0430 \u043f\u0435\u0440\u0435\u0432\u0456\u0440\u043a\u0438 Z-Wave. \u041f\u0435\u0440\u0435\u0432\u0456\u0440\u0442\u0435 \u0448\u043b\u044f\u0445 \u0434\u043e USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e." + }, + "step": { + "user": { + "data": { + "network_key": "\u041a\u043b\u044e\u0447 \u043c\u0435\u0440\u0435\u0436\u0456 (\u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438)", + "usb_path": "\u0428\u043b\u044f\u0445 \u0434\u043e USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" + }, + "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.", + "title": "Z-Wave" + } + } + }, "state": { "_": { - "dead": "\u041d\u0435\u0440\u043e\u0431\u043e\u0447\u0430", + "dead": "\u041d\u0435\u0441\u043f\u0440\u0430\u0432\u043d\u0438\u0439", "initializing": "\u0406\u043d\u0456\u0446\u0456\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u044f", "ready": "\u0413\u043e\u0442\u043e\u0432\u0438\u0439", - "sleeping": "\u0421\u043f\u043b\u044f\u0447\u043a\u0430" + "sleeping": "\u0420\u0435\u0436\u0438\u043c \u0441\u043d\u0443" }, "query_stage": { - "dead": "\u041d\u0435\u0440\u043e\u0431\u043e\u0447\u0430 ({query_stage})", - "initializing": "\u0406\u043d\u0456\u0446\u0456\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u044f ( {query_stage} )" + "dead": "\u041d\u0435\u0441\u043f\u0440\u0430\u0432\u043d\u0438\u0439", + "initializing": "\u0406\u043d\u0456\u0446\u0456\u0430\u043b\u0456\u0437\u0430\u0446\u0456\u044f" } } } \ No newline at end of file diff --git a/homeassistant/components/zwave/websocket_api.py b/homeassistant/components/zwave/websocket_api.py index 5e3a49df63c..bf84a27166e 100644 --- a/homeassistant/components/zwave/websocket_api.py +++ b/homeassistant/components/zwave/websocket_api.py @@ -2,6 +2,8 @@ import voluptuous as vol from homeassistant.components import websocket_api +from homeassistant.components.ozw.const import DOMAIN as OZW_DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.core import callback from .const import ( @@ -56,9 +58,32 @@ def websocket_get_migration_config(hass, connection, msg): ) +@websocket_api.require_admin +@websocket_api.async_response +@websocket_api.websocket_command({vol.Required(TYPE): "zwave/start_ozw_config_flow"}) +async def websocket_start_ozw_config_flow(hass, connection, msg): + """Start the ozw integration config flow (for migration wizard). + + Return data with the flow id of the started ozw config flow. + """ + config = hass.data[DATA_ZWAVE_CONFIG] + data = { + "usb_path": config[CONF_USB_STICK_PATH], + "network_key": config[CONF_NETWORK_KEY], + } + result = await hass.config_entries.flow.async_init( + OZW_DOMAIN, context={"source": SOURCE_IMPORT}, data=data + ) + connection.send_result( + msg[ID], + {"flow_id": result["flow_id"]}, + ) + + @callback def async_load_websocket_api(hass): """Set up the web socket API.""" websocket_api.async_register_command(hass, websocket_network_status) websocket_api.async_register_command(hass, websocket_get_config) websocket_api.async_register_command(hass, websocket_get_migration_config) + websocket_api.async_register_command(hass, websocket_start_ozw_config_flow) diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py new file mode 100644 index 00000000000..01b8f4785c5 --- /dev/null +++ b/homeassistant/components/zwave_js/__init__.py @@ -0,0 +1,343 @@ +"""The Z-Wave JS integration.""" +import asyncio +import logging +from typing import Callable, List + +from async_timeout import timeout +from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.exceptions import BaseZwaveJSServerError +from zwave_js_server.model.node import Node as ZwaveNode +from zwave_js_server.model.notification import Notification +from zwave_js_server.model.value import ValueNotification + +from homeassistant.components.hassio.handler import HassioAPIError +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_URL, EVENT_HOMEASSISTANT_STOP +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import device_registry +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .api import async_register_api +from .const import ( + ATTR_COMMAND_CLASS, + ATTR_COMMAND_CLASS_NAME, + ATTR_DEVICE_ID, + ATTR_DOMAIN, + ATTR_ENDPOINT, + ATTR_HOME_ID, + ATTR_LABEL, + ATTR_NODE_ID, + ATTR_PARAMETERS, + ATTR_PROPERTY_KEY_NAME, + ATTR_PROPERTY_NAME, + ATTR_TYPE, + ATTR_VALUE, + CONF_INTEGRATION_CREATED_ADDON, + DATA_CLIENT, + DATA_UNSUBSCRIBE, + DOMAIN, + EVENT_DEVICE_ADDED_TO_REGISTRY, + PLATFORMS, + ZWAVE_JS_EVENT, +) +from .discovery import async_discover_values +from .entity import get_device_id + +LOGGER = logging.getLogger(__package__) +CONNECT_TIMEOUT = 10 +DATA_CLIENT_LISTEN_TASK = "client_listen_task" +DATA_START_PLATFORM_TASK = "start_platform_task" + + +async def async_setup(hass: HomeAssistant, config: dict) -> bool: + """Set up the Z-Wave JS component.""" + hass.data[DOMAIN] = {} + return True + + +@callback +def register_node_in_dev_reg( + hass: HomeAssistant, + entry: ConfigEntry, + dev_reg: device_registry.DeviceRegistry, + client: ZwaveClient, + node: ZwaveNode, +) -> None: + """Register node in dev reg.""" + device = dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={get_device_id(client, node)}, + sw_version=node.firmware_version, + name=node.name or node.device_config.description or f"Node {node.node_id}", + model=node.device_config.label, + manufacturer=node.device_config.manufacturer, + ) + + async_dispatcher_send(hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Z-Wave JS from a config entry.""" + client = ZwaveClient(entry.data[CONF_URL], async_get_clientsession(hass)) + dev_reg = await device_registry.async_get_registry(hass) + + @callback + def async_on_node_ready(node: ZwaveNode) -> None: + """Handle node ready event.""" + LOGGER.debug("Processing node %s", node) + + # register (or update) node in device registry + register_node_in_dev_reg(hass, entry, dev_reg, client, node) + + # run discovery on all node values and create/update entities + for disc_info in async_discover_values(node): + LOGGER.debug("Discovered entity: %s", disc_info) + async_dispatcher_send( + hass, f"{DOMAIN}_{entry.entry_id}_add_{disc_info.platform}", disc_info + ) + # add listener for stateless node value notification events + node.on( + "value notification", + lambda event: async_on_value_notification(event["value_notification"]), + ) + # add listener for stateless node notification events + node.on( + "notification", lambda event: async_on_notification(event["notification"]) + ) + + @callback + def async_on_node_added(node: ZwaveNode) -> None: + """Handle node added event.""" + # we only want to run discovery when the node has reached ready state, + # otherwise we'll have all kinds of missing info issues. + if node.ready: + async_on_node_ready(node) + return + # if node is not yet ready, register one-time callback for ready state + LOGGER.debug("Node added: %s - waiting for it to become ready.", node.node_id) + node.once( + "ready", + lambda event: async_on_node_ready(event["node"]), + ) + # we do submit the node to device registry so user has + # some visual feedback that something is (in the process of) being added + register_node_in_dev_reg(hass, entry, dev_reg, client, node) + + @callback + def async_on_node_removed(node: ZwaveNode) -> None: + """Handle node removed event.""" + # grab device in device registry attached to this node + dev_id = get_device_id(client, node) + device = dev_reg.async_get_device({dev_id}) + # note: removal of entity registry is handled by core + dev_reg.async_remove_device(device.id) + + @callback + def async_on_value_notification(notification: ValueNotification) -> None: + """Relay stateless value notification events from Z-Wave nodes to hass.""" + device = dev_reg.async_get_device({get_device_id(client, notification.node)}) + value = notification.value + if notification.metadata.states: + value = notification.metadata.states.get(str(value), value) + hass.bus.async_fire( + ZWAVE_JS_EVENT, + { + ATTR_TYPE: "value_notification", + ATTR_DOMAIN: DOMAIN, + ATTR_NODE_ID: notification.node.node_id, + ATTR_HOME_ID: client.driver.controller.home_id, + ATTR_ENDPOINT: notification.endpoint, + ATTR_DEVICE_ID: device.id, + ATTR_COMMAND_CLASS: notification.command_class, + ATTR_COMMAND_CLASS_NAME: notification.command_class_name, + ATTR_LABEL: notification.metadata.label, + ATTR_PROPERTY_NAME: notification.property_name, + ATTR_PROPERTY_KEY_NAME: notification.property_key_name, + ATTR_VALUE: value, + }, + ) + + @callback + def async_on_notification(notification: Notification) -> None: + """Relay stateless notification events from Z-Wave nodes to hass.""" + device = dev_reg.async_get_device({get_device_id(client, notification.node)}) + hass.bus.async_fire( + ZWAVE_JS_EVENT, + { + ATTR_TYPE: "notification", + ATTR_DOMAIN: DOMAIN, + ATTR_NODE_ID: notification.node.node_id, + ATTR_HOME_ID: client.driver.controller.home_id, + ATTR_DEVICE_ID: device.id, + ATTR_LABEL: notification.notification_label, + ATTR_PARAMETERS: notification.parameters, + }, + ) + + # connect and throw error if connection failed + try: + async with timeout(CONNECT_TIMEOUT): + await client.connect() + except (asyncio.TimeoutError, BaseZwaveJSServerError) as err: + raise ConfigEntryNotReady from err + else: + LOGGER.info("Connected to Zwave JS Server") + + unsubscribe_callbacks: List[Callable] = [] + hass.data[DOMAIN][entry.entry_id] = { + DATA_CLIENT: client, + DATA_UNSUBSCRIBE: unsubscribe_callbacks, + } + + # Set up websocket API + async_register_api(hass) + + async def start_platforms() -> None: + """Start platforms and perform discovery.""" + # wait until all required platforms are ready + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_setup(entry, component) + for component in PLATFORMS + ] + ) + + driver_ready = asyncio.Event() + + async def handle_ha_shutdown(event: Event) -> None: + """Handle HA shutdown.""" + await disconnect_client(hass, entry, client, listen_task, platform_task) + + listen_task = asyncio.create_task( + client_listen(hass, entry, client, driver_ready) + ) + hass.data[DOMAIN][entry.entry_id][DATA_CLIENT_LISTEN_TASK] = listen_task + unsubscribe_callbacks.append( + hass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, handle_ha_shutdown) + ) + + await driver_ready.wait() + + LOGGER.info("Connection to Zwave JS Server initialized") + + # Check for nodes that no longer exist and remove them + stored_devices = device_registry.async_entries_for_config_entry( + dev_reg, entry.entry_id + ) + known_devices = [ + dev_reg.async_get_device({get_device_id(client, node)}) + for node in client.driver.controller.nodes.values() + ] + + # Devices that are in the device registry that are not known by the controller can be removed + for device in stored_devices: + if device not in known_devices: + dev_reg.async_remove_device(device.id) + + # run discovery on all ready nodes + for node in client.driver.controller.nodes.values(): + async_on_node_added(node) + + # listen for new nodes being added to the mesh + client.driver.controller.on( + "node added", lambda event: async_on_node_added(event["node"]) + ) + # listen for nodes being removed from the mesh + # NOTE: This will not remove nodes that were removed when HA was not running + client.driver.controller.on( + "node removed", lambda event: async_on_node_removed(event["node"]) + ) + + platform_task = hass.async_create_task(start_platforms()) + hass.data[DOMAIN][entry.entry_id][DATA_START_PLATFORM_TASK] = platform_task + + return True + + +async def client_listen( + hass: HomeAssistant, + entry: ConfigEntry, + client: ZwaveClient, + driver_ready: asyncio.Event, +) -> None: + """Listen with the client.""" + should_reload = True + try: + await client.listen(driver_ready) + except asyncio.CancelledError: + should_reload = False + except BaseZwaveJSServerError: + pass + + # The entry needs to be reloaded since a new driver state + # will be acquired on reconnect. + # All model instances will be replaced when the new state is acquired. + if should_reload: + LOGGER.info("Disconnected from server. Reloading integration") + asyncio.create_task(hass.config_entries.async_reload(entry.entry_id)) + + +async def disconnect_client( + hass: HomeAssistant, + entry: ConfigEntry, + client: ZwaveClient, + listen_task: asyncio.Task, + platform_task: asyncio.Task, +) -> None: + """Disconnect client.""" + listen_task.cancel() + platform_task.cancel() + + await asyncio.gather(listen_task, platform_task) + + if client.connected: + await client.disconnect() + LOGGER.info("Disconnected from Zwave JS Server") + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if not unload_ok: + return False + + info = hass.data[DOMAIN].pop(entry.entry_id) + + for unsub in info[DATA_UNSUBSCRIBE]: + unsub() + + if DATA_CLIENT_LISTEN_TASK in info: + await disconnect_client( + hass, + entry, + info[DATA_CLIENT], + info[DATA_CLIENT_LISTEN_TASK], + platform_task=info[DATA_START_PLATFORM_TASK], + ) + + return True + + +async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Remove a config entry.""" + if not entry.data.get(CONF_INTEGRATION_CREATED_ADDON): + return + + try: + await hass.components.hassio.async_stop_addon("core_zwave_js") + except HassioAPIError as err: + LOGGER.error("Failed to stop the Z-Wave JS add-on: %s", err) + return + try: + await hass.components.hassio.async_uninstall_addon("core_zwave_js") + except HassioAPIError as err: + LOGGER.error("Failed to uninstall the Z-Wave JS add-on: %s", err) diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py new file mode 100644 index 00000000000..03a917217a9 --- /dev/null +++ b/homeassistant/components/zwave_js/api.py @@ -0,0 +1,292 @@ +"""Websocket API for Z-Wave JS.""" +import json +import logging + +from aiohttp import hdrs, web, web_exceptions +import voluptuous as vol +from zwave_js_server import dump + +from homeassistant.components import websocket_api +from homeassistant.components.http.view import HomeAssistantView +from homeassistant.components.websocket_api.connection import ActiveConnection +from homeassistant.const import CONF_URL +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.device_registry import DeviceEntry +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_CLIENT, DOMAIN, EVENT_DEVICE_ADDED_TO_REGISTRY + +_LOGGER = logging.getLogger(__name__) + +ID = "id" +ENTRY_ID = "entry_id" +NODE_ID = "node_id" +TYPE = "type" + + +@callback +def async_register_api(hass: HomeAssistant) -> None: + """Register all of our api endpoints.""" + websocket_api.async_register_command(hass, websocket_network_status) + websocket_api.async_register_command(hass, websocket_node_status) + websocket_api.async_register_command(hass, websocket_add_node) + websocket_api.async_register_command(hass, websocket_stop_inclusion) + websocket_api.async_register_command(hass, websocket_remove_node) + websocket_api.async_register_command(hass, websocket_stop_exclusion) + hass.http.register_view(DumpView) # type: ignore + + +@websocket_api.require_admin +@websocket_api.websocket_command( + {vol.Required(TYPE): "zwave_js/network_status", vol.Required(ENTRY_ID): str} +) +@callback +def websocket_network_status( + hass: HomeAssistant, connection: ActiveConnection, msg: dict +) -> None: + """Get the status of the Z-Wave JS network.""" + entry_id = msg[ENTRY_ID] + client = hass.data[DOMAIN][entry_id][DATA_CLIENT] + data = { + "client": { + "ws_server_url": client.ws_server_url, + "state": "connected" if client.connected else "disconnected", + "driver_version": client.version.driver_version, + "server_version": client.version.server_version, + }, + "controller": { + "home_id": client.driver.controller.data["homeId"], + "nodes": list(client.driver.controller.nodes), + }, + } + connection.send_result( + msg[ID], + data, + ) + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/node_status", + vol.Required(ENTRY_ID): str, + vol.Required(NODE_ID): int, + } +) +@callback +def websocket_node_status( + hass: HomeAssistant, connection: ActiveConnection, msg: dict +) -> None: + """Get the status of a Z-Wave JS node.""" + entry_id = msg[ENTRY_ID] + client = hass.data[DOMAIN][entry_id][DATA_CLIENT] + node_id = msg[NODE_ID] + node = client.driver.controller.nodes[node_id] + data = { + "node_id": node.node_id, + "is_routing": node.is_routing, + "status": node.status, + "is_secure": node.is_secure, + "ready": node.ready, + } + connection.send_result( + msg[ID], + data, + ) + + +@websocket_api.require_admin # type: ignore +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/add_node", + vol.Required(ENTRY_ID): str, + vol.Optional("secure", default=False): bool, + } +) +async def websocket_add_node( + hass: HomeAssistant, connection: ActiveConnection, msg: dict +) -> None: + """Add a node to the Z-Wave network.""" + entry_id = msg[ENTRY_ID] + client = hass.data[DOMAIN][entry_id][DATA_CLIENT] + controller = client.driver.controller + include_non_secure = not msg["secure"] + + @callback + def async_cleanup() -> None: + """Remove signal listeners.""" + for unsub in unsubs: + unsub() + + @callback + def forward_event(event: dict) -> None: + connection.send_message( + websocket_api.event_message(msg[ID], {"event": event["event"]}) + ) + + @callback + def node_added(event: dict) -> None: + node = event["node"] + node_details = { + "node_id": node.node_id, + "status": node.status, + "ready": node.ready, + } + connection.send_message( + websocket_api.event_message( + msg[ID], {"event": "node added", "node": node_details} + ) + ) + + @callback + def device_registered(device: DeviceEntry) -> None: + device_details = {"name": device.name, "id": device.id} + connection.send_message( + websocket_api.event_message( + msg[ID], {"event": "device registered", "device": device_details} + ) + ) + + connection.subscriptions[msg["id"]] = async_cleanup + unsubs = [ + controller.on("inclusion started", forward_event), + controller.on("inclusion failed", forward_event), + controller.on("inclusion stopped", forward_event), + controller.on("node added", node_added), + async_dispatcher_connect( + hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device_registered + ), + ] + + result = await controller.async_begin_inclusion(include_non_secure) + connection.send_result( + msg[ID], + result, + ) + + +@websocket_api.require_admin # type: ignore +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/stop_inclusion", + vol.Required(ENTRY_ID): str, + } +) +async def websocket_stop_inclusion( + hass: HomeAssistant, connection: ActiveConnection, msg: dict +) -> None: + """Cancel adding a node to the Z-Wave network.""" + entry_id = msg[ENTRY_ID] + client = hass.data[DOMAIN][entry_id][DATA_CLIENT] + controller = client.driver.controller + result = await controller.async_stop_inclusion() + connection.send_result( + msg[ID], + result, + ) + + +@websocket_api.require_admin # type: ignore +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/stop_exclusion", + vol.Required(ENTRY_ID): str, + } +) +async def websocket_stop_exclusion( + hass: HomeAssistant, connection: ActiveConnection, msg: dict +) -> None: + """Cancel removing a node from the Z-Wave network.""" + entry_id = msg[ENTRY_ID] + client = hass.data[DOMAIN][entry_id][DATA_CLIENT] + controller = client.driver.controller + result = await controller.async_stop_exclusion() + connection.send_result( + msg[ID], + result, + ) + + +@websocket_api.require_admin # type:ignore +@websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/remove_node", + vol.Required(ENTRY_ID): str, + } +) +async def websocket_remove_node( + hass: HomeAssistant, connection: ActiveConnection, msg: dict +) -> None: + """Remove a node from the Z-Wave network.""" + entry_id = msg[ENTRY_ID] + client = hass.data[DOMAIN][entry_id][DATA_CLIENT] + controller = client.driver.controller + + @callback + def async_cleanup() -> None: + """Remove signal listeners.""" + for unsub in unsubs: + unsub() + + @callback + def forward_event(event: dict) -> None: + connection.send_message( + websocket_api.event_message(msg[ID], {"event": event["event"]}) + ) + + @callback + def node_removed(event: dict) -> None: + node = event["node"] + node_details = { + "node_id": node.node_id, + } + + connection.send_message( + websocket_api.event_message( + msg[ID], {"event": "node removed", "node": node_details} + ) + ) + + connection.subscriptions[msg["id"]] = async_cleanup + unsubs = [ + controller.on("exclusion started", forward_event), + controller.on("exclusion failed", forward_event), + controller.on("exclusion stopped", forward_event), + controller.on("node removed", node_removed), + ] + + result = await controller.async_begin_exclusion() + connection.send_result( + msg[ID], + result, + ) + + +class DumpView(HomeAssistantView): + """View to dump the state of the Z-Wave JS server.""" + + url = "/api/zwave_js/dump/{config_entry_id}" + name = "api:zwave_js:dump" + + async def get(self, request: web.Request, config_entry_id: str) -> web.Response: + """Dump the state of Z-Wave.""" + hass = request.app["hass"] + + if config_entry_id not in hass.data[DOMAIN]: + raise web_exceptions.HTTPBadRequest + + entry = hass.config_entries.async_get_entry(config_entry_id) + + msgs = await dump.dump_msgs(entry.data[CONF_URL], async_get_clientsession(hass)) + + return web.Response( + body="\n".join(json.dumps(msg) for msg in msgs) + "\n", + headers={ + hdrs.CONTENT_TYPE: "application/jsonl", + hdrs.CONTENT_DISPOSITION: 'attachment; filename="zwave_js_dump.jsonl"', + }, + ) diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py new file mode 100644 index 00000000000..f17d893e371 --- /dev/null +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -0,0 +1,381 @@ +"""Representation of Z-Wave binary sensors.""" + +import logging +from typing import Callable, List, Optional, TypedDict + +from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.const import CommandClass + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GAS, + DEVICE_CLASS_HEAT, + DEVICE_CLASS_LOCK, + DEVICE_CLASS_MOISTURE, + DEVICE_CLASS_MOTION, + DEVICE_CLASS_PROBLEM, + DEVICE_CLASS_SAFETY, + DEVICE_CLASS_SMOKE, + DEVICE_CLASS_SOUND, + DOMAIN as BINARY_SENSOR_DOMAIN, + BinarySensorEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN +from .discovery import ZwaveDiscoveryInfo +from .entity import ZWaveBaseEntity + +LOGGER = logging.getLogger(__name__) + + +NOTIFICATION_SMOKE_ALARM = 1 +NOTIFICATION_CARBON_MONOOXIDE = 2 +NOTIFICATION_CARBON_DIOXIDE = 3 +NOTIFICATION_HEAT = 4 +NOTIFICATION_WATER = 5 +NOTIFICATION_ACCESS_CONTROL = 6 +NOTIFICATION_HOME_SECURITY = 7 +NOTIFICATION_POWER_MANAGEMENT = 8 +NOTIFICATION_SYSTEM = 9 +NOTIFICATION_EMERGENCY = 10 +NOTIFICATION_CLOCK = 11 +NOTIFICATION_APPLIANCE = 12 +NOTIFICATION_HOME_HEALTH = 13 +NOTIFICATION_SIREN = 14 +NOTIFICATION_WATER_VALVE = 15 +NOTIFICATION_WEATHER = 16 +NOTIFICATION_IRRIGATION = 17 +NOTIFICATION_GAS = 18 + + +class NotificationSensorMapping(TypedDict, total=False): + """Represent a notification sensor mapping dict type.""" + + type: int # required + states: List[str] + device_class: str + enabled: bool + + +# Mappings for Notification sensors +# https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/notifications.json +NOTIFICATION_SENSOR_MAPPINGS: List[NotificationSensorMapping] = [ + { + # NotificationType 1: Smoke Alarm - State Id's 1 and 2 - Smoke detected + "type": NOTIFICATION_SMOKE_ALARM, + "states": ["1", "2"], + "device_class": DEVICE_CLASS_SMOKE, + }, + { + # NotificationType 1: Smoke Alarm - All other State Id's + "type": NOTIFICATION_SMOKE_ALARM, + "device_class": DEVICE_CLASS_PROBLEM, + }, + { + # NotificationType 2: Carbon Monoxide - State Id's 1 and 2 + "type": NOTIFICATION_CARBON_MONOOXIDE, + "states": ["1", "2"], + "device_class": DEVICE_CLASS_GAS, + }, + { + # NotificationType 2: Carbon Monoxide - All other State Id's + "type": NOTIFICATION_CARBON_MONOOXIDE, + "device_class": DEVICE_CLASS_PROBLEM, + }, + { + # NotificationType 3: Carbon Dioxide - State Id's 1 and 2 + "type": NOTIFICATION_CARBON_DIOXIDE, + "states": ["1", "2"], + "device_class": DEVICE_CLASS_GAS, + }, + { + # NotificationType 3: Carbon Dioxide - All other State Id's + "type": NOTIFICATION_CARBON_DIOXIDE, + "device_class": DEVICE_CLASS_PROBLEM, + }, + { + # NotificationType 4: Heat - State Id's 1, 2, 5, 6 (heat/underheat) + "type": NOTIFICATION_HEAT, + "states": ["1", "2", "5", "6"], + "device_class": DEVICE_CLASS_HEAT, + }, + { + # NotificationType 4: Heat - All other State Id's + "type": NOTIFICATION_HEAT, + "device_class": DEVICE_CLASS_PROBLEM, + }, + { + # NotificationType 5: Water - State Id's 1, 2, 3, 4 + "type": NOTIFICATION_WATER, + "states": ["1", "2", "3", "4"], + "device_class": DEVICE_CLASS_MOISTURE, + }, + { + # NotificationType 5: Water - All other State Id's + "type": NOTIFICATION_WATER, + "device_class": DEVICE_CLASS_PROBLEM, + }, + { + # NotificationType 6: Access Control - State Id's 1, 2, 3, 4 (Lock) + "type": NOTIFICATION_ACCESS_CONTROL, + "states": ["1", "2", "3", "4"], + "device_class": DEVICE_CLASS_LOCK, + }, + { + # NotificationType 6: Access Control - State Id 16 (door/window open) + "type": NOTIFICATION_ACCESS_CONTROL, + "states": ["22"], + "device_class": DEVICE_CLASS_DOOR, + }, + { + # NotificationType 6: Access Control - State Id 17 (door/window closed) + "type": NOTIFICATION_ACCESS_CONTROL, + "states": ["23"], + "enabled": False, + }, + { + # NotificationType 7: Home Security - State Id's 1, 2 (intrusion) + "type": NOTIFICATION_HOME_SECURITY, + "states": ["1", "2"], + "device_class": DEVICE_CLASS_SAFETY, + }, + { + # NotificationType 7: Home Security - State Id's 3, 4, 9 (tampering) + "type": NOTIFICATION_HOME_SECURITY, + "states": ["3", "4", "9"], + "device_class": DEVICE_CLASS_SAFETY, + }, + { + # NotificationType 7: Home Security - State Id's 5, 6 (glass breakage) + "type": NOTIFICATION_HOME_SECURITY, + "states": ["5", "6"], + "device_class": DEVICE_CLASS_SAFETY, + }, + { + # NotificationType 7: Home Security - State Id's 7, 8 (motion) + "type": NOTIFICATION_HOME_SECURITY, + "states": ["7", "8"], + "device_class": DEVICE_CLASS_MOTION, + }, + { + # NotificationType 9: System - State Id's 1, 2, 6, 7 + "type": NOTIFICATION_SYSTEM, + "states": ["1", "2", "6", "7"], + "device_class": DEVICE_CLASS_PROBLEM, + }, + { + # NotificationType 10: Emergency - State Id's 1, 2, 3 + "type": NOTIFICATION_EMERGENCY, + "states": ["1", "2", "3"], + "device_class": DEVICE_CLASS_PROBLEM, + }, + { + # NotificationType 14: Siren + "type": NOTIFICATION_SIREN, + "states": ["1"], + "device_class": DEVICE_CLASS_SOUND, + }, + { + # NotificationType 18: Gas + "type": NOTIFICATION_GAS, + "states": ["1", "2", "3", "4"], + "device_class": DEVICE_CLASS_GAS, + }, + { + # NotificationType 18: Gas + "type": NOTIFICATION_GAS, + "states": ["6"], + "device_class": DEVICE_CLASS_PROBLEM, + }, +] + + +PROPERTY_DOOR_STATUS = "doorStatus" + + +class PropertySensorMapping(TypedDict, total=False): + """Represent a property sensor mapping dict type.""" + + property_name: str # required + on_states: List[str] # required + device_class: str + enabled: bool + + +# Mappings for property sensors +PROPERTY_SENSOR_MAPPINGS: List[PropertySensorMapping] = [ + { + "property_name": PROPERTY_DOOR_STATUS, + "on_states": ["open"], + "device_class": DEVICE_CLASS_DOOR, + "enabled": True, + }, +] + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up Z-Wave binary sensor from config entry.""" + client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + + @callback + def async_add_binary_sensor(info: ZwaveDiscoveryInfo) -> None: + """Add Z-Wave Binary Sensor.""" + entities: List[BinarySensorEntity] = [] + + if info.platform_hint == "notification": + # Get all sensors from Notification CC states + for state_key in info.primary_value.metadata.states: + # ignore idle key (0) + if state_key == "0": + continue + entities.append( + ZWaveNotificationBinarySensor(config_entry, client, info, state_key) + ) + elif info.platform_hint == "property": + entities.append(ZWavePropertyBinarySensor(config_entry, client, info)) + else: + # boolean sensor + entities.append(ZWaveBooleanBinarySensor(config_entry, client, info)) + + async_add_entities(entities) + + hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + async_dispatcher_connect( + hass, + f"{DOMAIN}_{config_entry.entry_id}_add_{BINARY_SENSOR_DOMAIN}", + async_add_binary_sensor, + ) + ) + + +class ZWaveBooleanBinarySensor(ZWaveBaseEntity, BinarySensorEntity): + """Representation of a Z-Wave binary_sensor.""" + + @property + def is_on(self) -> bool: + """Return if the sensor is on or off.""" + return bool(self.info.primary_value.value) + + @property + def device_class(self) -> Optional[str]: + """Return device class.""" + if self.info.primary_value.command_class == CommandClass.BATTERY: + return DEVICE_CLASS_BATTERY + return None + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + if self.info.primary_value.command_class == CommandClass.SENSOR_BINARY: + # Legacy binary sensors are phased out (replaced by notification sensors) + # Disable by default to not confuse users + if self.info.node.device_class.generic != "Binary Sensor": + return False + return True + + +class ZWaveNotificationBinarySensor(ZWaveBaseEntity, BinarySensorEntity): + """Representation of a Z-Wave binary_sensor from Notification CommandClass.""" + + def __init__( + self, + config_entry: ConfigEntry, + client: ZwaveClient, + info: ZwaveDiscoveryInfo, + state_key: str, + ) -> None: + """Initialize a ZWaveNotificationBinarySensor entity.""" + super().__init__(config_entry, client, info) + self.state_key = state_key + # check if we have a custom mapping for this value + self._mapping_info = self._get_sensor_mapping() + + @property + def is_on(self) -> bool: + """Return if the sensor is on or off.""" + return int(self.info.primary_value.value) == int(self.state_key) + + @property + def name(self) -> str: + """Return default name from device name and value name combination.""" + node_name = self.info.node.name or self.info.node.device_config.description + value_name = self.info.primary_value.property_name + state_label = self.info.primary_value.metadata.states[self.state_key] + return f"{node_name}: {value_name} - {state_label}" + + @property + def device_class(self) -> Optional[str]: + """Return device class.""" + return self._mapping_info.get("device_class") + + @property + def unique_id(self) -> str: + """Return unique id for this entity.""" + return f"{super().unique_id}.{self.state_key}" + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + if not self._mapping_info: + return True + return self._mapping_info.get("enabled", True) + + @callback + def _get_sensor_mapping(self) -> NotificationSensorMapping: + """Try to get a device specific mapping for this sensor.""" + for mapping in NOTIFICATION_SENSOR_MAPPINGS: + if ( + mapping["type"] + != self.info.primary_value.metadata.cc_specific["notificationType"] + ): + continue + if not mapping.get("states") or self.state_key in mapping["states"]: + # match found + return mapping + return {} + + +class ZWavePropertyBinarySensor(ZWaveBaseEntity, BinarySensorEntity): + """Representation of a Z-Wave binary_sensor from a property.""" + + def __init__( + self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + ) -> None: + """Initialize a ZWavePropertyBinarySensor entity.""" + super().__init__(config_entry, client, info) + # check if we have a custom mapping for this value + self._mapping_info = self._get_sensor_mapping() + + @property + def is_on(self) -> bool: + """Return if the sensor is on or off.""" + return self.info.primary_value.value in self._mapping_info["on_states"] + + @property + def device_class(self) -> Optional[str]: + """Return device class.""" + return self._mapping_info.get("device_class") + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + # We hide some more advanced sensors by default to not overwhelm users + # unless explicitly stated in a mapping, assume deisabled by default + return self._mapping_info.get("enabled", False) + + @callback + def _get_sensor_mapping(self) -> PropertySensorMapping: + """Try to get a device specific mapping for this sensor.""" + mapping_info = PropertySensorMapping() + for mapping in PROPERTY_SENSOR_MAPPINGS: + if mapping["property_name"] == self.info.primary_value.property_name: + mapping_info = mapping.copy() + break + + return mapping_info diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py new file mode 100644 index 00000000000..b125c8bcd6a --- /dev/null +++ b/homeassistant/components/zwave_js/climate.py @@ -0,0 +1,325 @@ +"""Representation of Z-Wave thermostats.""" +import logging +from typing import Any, Callable, Dict, List, Optional + +from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.const import ( + THERMOSTAT_CURRENT_TEMP_PROPERTY, + THERMOSTAT_MODE_PROPERTY, + THERMOSTAT_MODE_SETPOINT_MAP, + THERMOSTAT_MODES, + THERMOSTAT_OPERATING_STATE_PROPERTY, + THERMOSTAT_SETPOINT_PROPERTY, + CommandClass, + ThermostatMode, + ThermostatOperatingState, + ThermostatSetpointType, +) +from zwave_js_server.model.value import Value as ZwaveValue + +from homeassistant.components.climate import ClimateEntity +from homeassistant.components.climate.const import ( + ATTR_HVAC_MODE, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + CURRENT_HVAC_COOL, + CURRENT_HVAC_FAN, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + DOMAIN as CLIMATE_DOMAIN, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + PRESET_NONE, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN +from .discovery import ZwaveDiscoveryInfo +from .entity import ZWaveBaseEntity + +_LOGGER = logging.getLogger(__name__) + +# Map Z-Wave HVAC Mode to Home Assistant value +# Note: We treat "auto" as "heat_cool" as most Z-Wave devices +# report auto_changeover as auto without schedule support. +ZW_HVAC_MODE_MAP: Dict[int, str] = { + ThermostatMode.OFF: HVAC_MODE_OFF, + ThermostatMode.HEAT: HVAC_MODE_HEAT, + ThermostatMode.COOL: HVAC_MODE_COOL, + # Z-Wave auto mode is actually heat/cool in the hass world + ThermostatMode.AUTO: HVAC_MODE_HEAT_COOL, + ThermostatMode.AUXILIARY: HVAC_MODE_HEAT, + ThermostatMode.FAN: HVAC_MODE_FAN_ONLY, + ThermostatMode.FURNANCE: HVAC_MODE_HEAT, + ThermostatMode.DRY: HVAC_MODE_DRY, + ThermostatMode.AUTO_CHANGE_OVER: HVAC_MODE_HEAT_COOL, + ThermostatMode.HEATING_ECON: HVAC_MODE_HEAT, + ThermostatMode.COOLING_ECON: HVAC_MODE_COOL, + ThermostatMode.AWAY: HVAC_MODE_HEAT_COOL, + ThermostatMode.FULL_POWER: HVAC_MODE_HEAT, +} + +HVAC_CURRENT_MAP: Dict[int, str] = { + ThermostatOperatingState.IDLE: CURRENT_HVAC_IDLE, + ThermostatOperatingState.PENDING_HEAT: CURRENT_HVAC_IDLE, + ThermostatOperatingState.HEATING: CURRENT_HVAC_HEAT, + ThermostatOperatingState.PENDING_COOL: CURRENT_HVAC_IDLE, + ThermostatOperatingState.COOLING: CURRENT_HVAC_COOL, + ThermostatOperatingState.FAN_ONLY: CURRENT_HVAC_FAN, + ThermostatOperatingState.VENT_ECONOMIZER: CURRENT_HVAC_FAN, + ThermostatOperatingState.AUX_HEATING: CURRENT_HVAC_HEAT, + ThermostatOperatingState.SECOND_STAGE_HEATING: CURRENT_HVAC_HEAT, + ThermostatOperatingState.SECOND_STAGE_COOLING: CURRENT_HVAC_COOL, + ThermostatOperatingState.SECOND_STAGE_AUX_HEAT: CURRENT_HVAC_HEAT, + ThermostatOperatingState.THIRD_STAGE_AUX_HEAT: CURRENT_HVAC_HEAT, +} + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up Z-Wave climate from config entry.""" + client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + + @callback + def async_add_climate(info: ZwaveDiscoveryInfo) -> None: + """Add Z-Wave Climate.""" + entities: List[ZWaveBaseEntity] = [] + entities.append(ZWaveClimate(config_entry, client, info)) + + async_add_entities(entities) + + hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + async_dispatcher_connect( + hass, + f"{DOMAIN}_{config_entry.entry_id}_add_{CLIMATE_DOMAIN}", + async_add_climate, + ) + ) + + +class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): + """Representation of a Z-Wave climate.""" + + def __init__( + self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + ) -> None: + """Initialize lock.""" + super().__init__(config_entry, client, info) + self._hvac_modes: Dict[str, Optional[int]] = {} + self._hvac_presets: Dict[str, Optional[int]] = {} + self._unit_value: ZwaveValue = None + + self._current_mode = self.get_zwave_value( + THERMOSTAT_MODE_PROPERTY, command_class=CommandClass.THERMOSTAT_MODE + ) + self._setpoint_values: Dict[ThermostatSetpointType, ZwaveValue] = {} + for enum in ThermostatSetpointType: + self._setpoint_values[enum] = self.get_zwave_value( + THERMOSTAT_SETPOINT_PROPERTY, + command_class=CommandClass.THERMOSTAT_SETPOINT, + value_property_key_name=enum.value, + add_to_watched_value_ids=True, + ) + # Use the first found setpoint value to always determine the temperature unit + if self._setpoint_values[enum] and not self._unit_value: + self._unit_value = self._setpoint_values[enum] + self._operating_state = self.get_zwave_value( + THERMOSTAT_OPERATING_STATE_PROPERTY, + command_class=CommandClass.THERMOSTAT_OPERATING_STATE, + add_to_watched_value_ids=True, + ) + self._current_temp = self.get_zwave_value( + THERMOSTAT_CURRENT_TEMP_PROPERTY, + command_class=CommandClass.SENSOR_MULTILEVEL, + add_to_watched_value_ids=True, + check_all_endpoints=True, + ) + self._current_humidity = self.get_zwave_value( + "Humidity", + command_class=CommandClass.SENSOR_MULTILEVEL, + add_to_watched_value_ids=True, + check_all_endpoints=True, + ) + self._set_modes_and_presets() + + def _setpoint_value(self, setpoint_type: ThermostatSetpointType) -> ZwaveValue: + """Optionally return a ZwaveValue for a setpoint.""" + val = self._setpoint_values[setpoint_type] + if val is None: + raise ValueError("Value requested is not available") + + return val + + def _set_modes_and_presets(self) -> None: + """Convert Z-Wave Thermostat modes into Home Assistant modes and presets.""" + all_modes: Dict[str, Optional[int]] = {} + all_presets: Dict[str, Optional[int]] = {PRESET_NONE: None} + + # Z-Wave uses one list for both modes and presets. + # Iterate over all Z-Wave ThermostatModes and extract the hvac modes and presets. + if self._current_mode is None: + self._hvac_modes = { + ZW_HVAC_MODE_MAP[ThermostatMode.HEAT]: ThermostatMode.HEAT + } + return + for mode_id, mode_name in self._current_mode.metadata.states.items(): + mode_id = int(mode_id) + if mode_id in THERMOSTAT_MODES: + # treat value as hvac mode + hass_mode = ZW_HVAC_MODE_MAP.get(mode_id) + if hass_mode: + all_modes[hass_mode] = mode_id + else: + # treat value as hvac preset + all_presets[mode_name] = mode_id + self._hvac_modes = all_modes + self._hvac_presets = all_presets + + @property + def _current_mode_setpoint_enums(self) -> List[Optional[ThermostatSetpointType]]: + """Return the list of enums that are relevant to the current thermostat mode.""" + if self._current_mode is None: + # Thermostat(valve) with no support for setting a mode is considered heating-only + return [ThermostatSetpointType.HEATING] + return THERMOSTAT_MODE_SETPOINT_MAP.get(int(self._current_mode.value), []) # type: ignore + + @property + def temperature_unit(self) -> str: + """Return the unit of measurement used by the platform.""" + if "f" in self._unit_value.metadata.unit.lower(): + return TEMP_FAHRENHEIT + return TEMP_CELSIUS + + @property + def hvac_mode(self) -> str: + """Return hvac operation ie. heat, cool mode.""" + if self._current_mode is None: + # Thermostat(valve) with no support for setting a mode is considered heating-only + return HVAC_MODE_HEAT + return ZW_HVAC_MODE_MAP.get(int(self._current_mode.value), HVAC_MODE_HEAT_COOL) + + @property + def hvac_modes(self) -> List[str]: + """Return the list of available hvac operation modes.""" + return list(self._hvac_modes) + + @property + def hvac_action(self) -> Optional[str]: + """Return the current running hvac operation if supported.""" + if not self._operating_state: + return None + return HVAC_CURRENT_MAP.get(int(self._operating_state.value)) + + @property + def current_humidity(self) -> Optional[int]: + """Return the current humidity level.""" + return self._current_humidity.value if self._current_humidity else None + + @property + def current_temperature(self) -> Optional[float]: + """Return the current temperature.""" + return self._current_temp.value if self._current_temp else None + + @property + def target_temperature(self) -> Optional[float]: + """Return the temperature we try to reach.""" + temp = self._setpoint_value(self._current_mode_setpoint_enums[0]) + return temp.value if temp else None + + @property + def target_temperature_high(self) -> Optional[float]: + """Return the highbound target temperature we try to reach.""" + temp = self._setpoint_value(self._current_mode_setpoint_enums[1]) + return temp.value if temp else None + + @property + def target_temperature_low(self) -> Optional[float]: + """Return the lowbound target temperature we try to reach.""" + return self.target_temperature + + @property + def preset_mode(self) -> Optional[str]: + """Return the current preset mode, e.g., home, away, temp.""" + if self._current_mode and int(self._current_mode.value) not in THERMOSTAT_MODES: + return_val: str = self._current_mode.metadata.states.get( + self._current_mode.value + ) + return return_val + return PRESET_NONE + + @property + def preset_modes(self) -> Optional[List[str]]: + """Return a list of available preset modes.""" + return list(self._hvac_presets) + + @property + def supported_features(self) -> int: + """Return the list of supported features.""" + support = SUPPORT_PRESET_MODE + if len(self._current_mode_setpoint_enums) == 1: + support |= SUPPORT_TARGET_TEMPERATURE + if len(self._current_mode_setpoint_enums) > 1: + support |= SUPPORT_TARGET_TEMPERATURE_RANGE + return support + + async def async_set_temperature(self, **kwargs: Any) -> None: + """Set new target temperature.""" + assert self.hass + hvac_mode: Optional[str] = kwargs.get(ATTR_HVAC_MODE) + + if hvac_mode is not None: + await self.async_set_hvac_mode(hvac_mode) + if len(self._current_mode_setpoint_enums) == 1: + setpoint: ZwaveValue = self._setpoint_value( + self._current_mode_setpoint_enums[0] + ) + target_temp: Optional[float] = kwargs.get(ATTR_TEMPERATURE) + if target_temp is not None: + await self.info.node.async_set_value(setpoint, target_temp) + elif len(self._current_mode_setpoint_enums) == 2: + setpoint_low: ZwaveValue = self._setpoint_value( + self._current_mode_setpoint_enums[0] + ) + setpoint_high: ZwaveValue = self._setpoint_value( + self._current_mode_setpoint_enums[1] + ) + target_temp_low: Optional[float] = kwargs.get(ATTR_TARGET_TEMP_LOW) + target_temp_high: Optional[float] = kwargs.get(ATTR_TARGET_TEMP_HIGH) + if target_temp_low is not None: + await self.info.node.async_set_value(setpoint_low, target_temp_low) + if target_temp_high is not None: + await self.info.node.async_set_value(setpoint_high, target_temp_high) + + async def async_set_hvac_mode(self, hvac_mode: str) -> None: + """Set new target hvac mode.""" + if not self._current_mode: + # Thermostat(valve) with no support for setting a mode + raise ValueError( + f"Thermostat {self.entity_id} does not support setting a mode" + ) + hvac_mode_value = self._hvac_modes.get(hvac_mode) + if hvac_mode_value is None: + raise ValueError(f"Received an invalid hvac mode: {hvac_mode}") + await self.info.node.async_set_value(self._current_mode, hvac_mode_value) + + async def async_set_preset_mode(self, preset_mode: str) -> None: + """Set new target preset mode.""" + if preset_mode == PRESET_NONE: + # try to restore to the (translated) main hvac mode + await self.async_set_hvac_mode(self.hvac_mode) + return + preset_mode_value = self._hvac_presets.get(preset_mode) + if preset_mode_value is None: + raise ValueError(f"Received an invalid preset mode: {preset_mode}") + await self.info.node.async_set_value(self._current_mode, preset_mode_value) diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py new file mode 100644 index 00000000000..5faaa02d03d --- /dev/null +++ b/homeassistant/components/zwave_js/config_flow.py @@ -0,0 +1,382 @@ +"""Config flow for Z-Wave JS integration.""" +import asyncio +import logging +from typing import Any, Dict, Optional, cast + +import aiohttp +from async_timeout import timeout +import voluptuous as vol +from zwave_js_server.version import VersionInfo, get_server_version + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_URL +from homeassistant.data_entry_flow import AbortFlow +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import ( # pylint:disable=unused-import + CONF_INTEGRATION_CREATED_ADDON, + CONF_USE_ADDON, + DOMAIN, +) + +_LOGGER = logging.getLogger(__name__) + +CONF_ADDON_DEVICE = "device" +CONF_ADDON_NETWORK_KEY = "network_key" +CONF_NETWORK_KEY = "network_key" +CONF_USB_PATH = "usb_path" +DEFAULT_URL = "ws://localhost:3000" +TITLE = "Z-Wave JS" + +ADDON_SETUP_TIME = 10 + +ON_SUPERVISOR_SCHEMA = vol.Schema({vol.Optional(CONF_USE_ADDON, default=True): bool}) +STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_URL, default=DEFAULT_URL): str}) + + +async def validate_input(hass: core.HomeAssistant, user_input: dict) -> VersionInfo: + """Validate if the user input allows us to connect.""" + ws_address = user_input[CONF_URL] + + if not ws_address.startswith(("ws://", "wss://")): + raise InvalidInput("invalid_ws_url") + + try: + return await async_get_version_info(hass, ws_address) + except CannotConnect as err: + raise InvalidInput("cannot_connect") from err + + +async def async_get_version_info( + hass: core.HomeAssistant, ws_address: str +) -> VersionInfo: + """Return Z-Wave JS version info.""" + async with timeout(10): + try: + version_info: VersionInfo = await get_server_version( + ws_address, async_get_clientsession(hass) + ) + except (asyncio.TimeoutError, aiohttp.ClientError) as err: + _LOGGER.error("Failed to connect to Z-Wave JS server: %s", err) + raise CannotConnect from err + + return version_info + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Z-Wave JS.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + def __init__(self) -> None: + """Set up flow instance.""" + self.addon_config: Optional[dict] = None + self.network_key: Optional[str] = None + self.usb_path: Optional[str] = None + self.use_addon = False + self.ws_address: Optional[str] = None + # If we install the add-on we should uninstall it on entry remove. + self.integration_created_addon = False + self.install_task: Optional[asyncio.Task] = None + + async def async_step_user( + self, user_input: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """Handle the initial step.""" + assert self.hass # typing + if self.hass.components.hassio.is_hassio(): + return await self.async_step_on_supervisor() + + return await self.async_step_manual() + + async def async_step_manual( + self, user_input: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """Handle a manual configuration.""" + if user_input is None: + return self.async_show_form( + step_id="manual", data_schema=STEP_USER_DATA_SCHEMA + ) + + errors = {} + + assert self.hass # typing + try: + version_info = await validate_input(self.hass, user_input) + except InvalidInput as err: + errors["base"] = err.error + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id( + version_info.home_id, raise_on_progress=False + ) + self._abort_if_unique_id_configured(user_input) + self.ws_address = user_input[CONF_URL] + return self._async_create_entry_from_vars() + + return self.async_show_form( + step_id="manual", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) + + async def async_step_hassio( # type: ignore + self, discovery_info: Dict[str, Any] + ) -> Dict[str, Any]: + """Receive configuration from add-on discovery info. + + This flow is triggered by the Z-Wave JS add-on. + """ + assert self.hass + self.ws_address = f"ws://{discovery_info['host']}:{discovery_info['port']}" + try: + version_info = await async_get_version_info(self.hass, self.ws_address) + except CannotConnect: + return self.async_abort(reason="cannot_connect") + + await self.async_set_unique_id(version_info.home_id) + self._abort_if_unique_id_configured(updates={CONF_URL: self.ws_address}) + + return await self.async_step_hassio_confirm() + + async def async_step_hassio_confirm( + self, user_input: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """Confirm the add-on discovery.""" + if user_input is not None: + return await self.async_step_on_supervisor( + user_input={CONF_USE_ADDON: True} + ) + + return self.async_show_form(step_id="hassio_confirm") + + def _async_create_entry_from_vars(self) -> Dict[str, Any]: + """Return a config entry for the flow.""" + return self.async_create_entry( + title=TITLE, + data={ + CONF_URL: self.ws_address, + CONF_USB_PATH: self.usb_path, + CONF_NETWORK_KEY: self.network_key, + CONF_USE_ADDON: self.use_addon, + CONF_INTEGRATION_CREATED_ADDON: self.integration_created_addon, + }, + ) + + async def async_step_on_supervisor( + self, user_input: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """Handle logic when on Supervisor host.""" + if user_input is None: + return self.async_show_form( + step_id="on_supervisor", data_schema=ON_SUPERVISOR_SCHEMA + ) + if not user_input[CONF_USE_ADDON]: + return await self.async_step_manual() + + self.use_addon = True + + if await self._async_is_addon_running(): + discovery_info = await self._async_get_addon_discovery_info() + self.ws_address = f"ws://{discovery_info['host']}:{discovery_info['port']}" + + if not self.unique_id: + assert self.hass + try: + version_info = await async_get_version_info( + self.hass, self.ws_address + ) + except CannotConnect: + return self.async_abort(reason="cannot_connect") + await self.async_set_unique_id( + version_info.home_id, raise_on_progress=False + ) + + self._abort_if_unique_id_configured() + addon_config = await self._async_get_addon_config() + self.usb_path = addon_config[CONF_ADDON_DEVICE] + self.network_key = addon_config.get(CONF_ADDON_NETWORK_KEY, "") + return self._async_create_entry_from_vars() + + if await self._async_is_addon_installed(): + return await self.async_step_start_addon() + + return await self.async_step_install_addon() + + async def async_step_install_addon( + self, user_input: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """Install Z-Wave JS add-on.""" + assert self.hass + if not self.install_task: + self.install_task = self.hass.async_create_task(self._async_install_addon()) + return self.async_show_progress( + step_id="install_addon", progress_action="install_addon" + ) + + assert self.hass + try: + await self.install_task + except self.hass.components.hassio.HassioAPIError as err: + _LOGGER.error("Failed to install Z-Wave JS add-on: %s", err) + return self.async_show_progress_done(next_step_id="install_failed") + + self.integration_created_addon = True + + return self.async_show_progress_done(next_step_id="start_addon") + + async def async_step_install_failed( + self, user_input: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """Add-on installation failed.""" + return self.async_abort(reason="addon_install_failed") + + async def async_step_start_addon( + self, user_input: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """Ask for config and start Z-Wave JS add-on.""" + if self.addon_config is None: + self.addon_config = await self._async_get_addon_config() + + errors = {} + + if user_input is not None: + self.network_key = user_input[CONF_NETWORK_KEY] + self.usb_path = user_input[CONF_USB_PATH] + + new_addon_config = { + CONF_ADDON_DEVICE: self.usb_path, + CONF_ADDON_NETWORK_KEY: self.network_key, + } + + if new_addon_config != self.addon_config: + await self._async_set_addon_config(new_addon_config) + + assert self.hass + try: + await self.hass.components.hassio.async_start_addon("core_zwave_js") + except self.hass.components.hassio.HassioAPIError as err: + _LOGGER.error("Failed to start Z-Wave JS add-on: %s", err) + errors["base"] = "addon_start_failed" + else: + # Sleep some seconds to let the add-on start properly before connecting. + await asyncio.sleep(ADDON_SETUP_TIME) + discovery_info = await self._async_get_addon_discovery_info() + self.ws_address = ( + f"ws://{discovery_info['host']}:{discovery_info['port']}" + ) + + if not self.unique_id: + try: + version_info = await async_get_version_info( + self.hass, self.ws_address + ) + except CannotConnect: + return self.async_abort(reason="cannot_connect") + await self.async_set_unique_id( + version_info.home_id, raise_on_progress=False + ) + + self._abort_if_unique_id_configured() + return self._async_create_entry_from_vars() + + usb_path = self.addon_config.get(CONF_ADDON_DEVICE, self.usb_path or "") + network_key = self.addon_config.get( + CONF_ADDON_NETWORK_KEY, self.network_key or "" + ) + + data_schema = vol.Schema( + { + vol.Required(CONF_USB_PATH, default=usb_path): str, + vol.Optional(CONF_NETWORK_KEY, default=network_key): str, + } + ) + + return self.async_show_form( + step_id="start_addon", data_schema=data_schema, errors=errors + ) + + async def _async_get_addon_info(self) -> dict: + """Return and cache Z-Wave JS add-on info.""" + assert self.hass + try: + addon_info: dict = await self.hass.components.hassio.async_get_addon_info( + "core_zwave_js" + ) + except self.hass.components.hassio.HassioAPIError as err: + _LOGGER.error("Failed to get Z-Wave JS add-on info: %s", err) + raise AbortFlow("addon_info_failed") from err + + return addon_info + + async def _async_is_addon_running(self) -> bool: + """Return True if Z-Wave JS add-on is running.""" + addon_info = await self._async_get_addon_info() + return bool(addon_info["state"] == "started") + + async def _async_is_addon_installed(self) -> bool: + """Return True if Z-Wave JS add-on is installed.""" + addon_info = await self._async_get_addon_info() + return addon_info["version"] is not None + + async def _async_get_addon_config(self) -> dict: + """Get Z-Wave JS add-on config.""" + addon_info = await self._async_get_addon_info() + return cast(dict, addon_info["options"]) + + async def _async_set_addon_config(self, config: dict) -> None: + """Set Z-Wave JS add-on config.""" + assert self.hass + options = {"options": config} + try: + await self.hass.components.hassio.async_set_addon_options( + "core_zwave_js", options + ) + except self.hass.components.hassio.HassioAPIError as err: + _LOGGER.error("Failed to set Z-Wave JS add-on config: %s", err) + raise AbortFlow("addon_set_config_failed") from err + + async def _async_install_addon(self) -> None: + """Install the Z-Wave JS add-on.""" + assert self.hass + try: + await self.hass.components.hassio.async_install_addon("core_zwave_js") + finally: + # Continue the flow after show progress when the task is done. + self.hass.async_create_task( + self.hass.config_entries.flow.async_configure(flow_id=self.flow_id) + ) + + async def _async_get_addon_discovery_info(self) -> dict: + """Return add-on discovery info.""" + assert self.hass + try: + discovery_info: dict = ( + await self.hass.components.hassio.async_get_addon_discovery_info( + "core_zwave_js" + ) + ) + except self.hass.components.hassio.HassioAPIError as err: + _LOGGER.error("Failed to get Z-Wave JS add-on discovery info: %s", err) + raise AbortFlow("addon_get_discovery_info_failed") from err + + if not discovery_info: + _LOGGER.error("Failed to get Z-Wave JS add-on discovery info") + raise AbortFlow("addon_missing_discovery_info") + + discovery_info_config: dict = discovery_info["config"] + return discovery_info_config + + +class CannotConnect(exceptions.HomeAssistantError): + """Indicate connection error.""" + + +class InvalidInput(exceptions.HomeAssistantError): + """Error to indicate input data is invalid.""" + + def __init__(self, error: str) -> None: + """Initialize error.""" + super().__init__() + self.error = error diff --git a/homeassistant/components/zwave_js/const.py b/homeassistant/components/zwave_js/const.py new file mode 100644 index 00000000000..dc2ffaeaa20 --- /dev/null +++ b/homeassistant/components/zwave_js/const.py @@ -0,0 +1,35 @@ +"""Constants for the Z-Wave JS integration.""" +CONF_INTEGRATION_CREATED_ADDON = "integration_created_addon" +CONF_USE_ADDON = "use_addon" +DOMAIN = "zwave_js" +PLATFORMS = [ + "binary_sensor", + "climate", + "cover", + "fan", + "light", + "lock", + "sensor", + "switch", +] + +DATA_CLIENT = "client" +DATA_UNSUBSCRIBE = "unsubs" + +EVENT_DEVICE_ADDED_TO_REGISTRY = f"{DOMAIN}_device_added_to_registry" + +# constants for events +ZWAVE_JS_EVENT = f"{DOMAIN}_event" +ATTR_NODE_ID = "node_id" +ATTR_HOME_ID = "home_id" +ATTR_ENDPOINT = "endpoint" +ATTR_LABEL = "label" +ATTR_VALUE = "value" +ATTR_COMMAND_CLASS = "command_class" +ATTR_COMMAND_CLASS_NAME = "command_class_name" +ATTR_TYPE = "type" +ATTR_DOMAIN = "domain" +ATTR_DEVICE_ID = "device_id" +ATTR_PROPERTY_NAME = "property_name" +ATTR_PROPERTY_KEY_NAME = "property_key_name" +ATTR_PARAMETERS = "parameters" diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py new file mode 100644 index 00000000000..5f473f80957 --- /dev/null +++ b/homeassistant/components/zwave_js/cover.py @@ -0,0 +1,95 @@ +"""Support for Z-Wave cover devices.""" +import logging +from typing import Any, Callable, List + +from zwave_js_server.client import Client as ZwaveClient + +from homeassistant.components.cover import ( + ATTR_POSITION, + DOMAIN as COVER_DOMAIN, + SUPPORT_CLOSE, + SUPPORT_OPEN, + CoverEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN +from .discovery import ZwaveDiscoveryInfo +from .entity import ZWaveBaseEntity + +LOGGER = logging.getLogger(__name__) +SUPPORT_GARAGE = SUPPORT_OPEN | SUPPORT_CLOSE +PRESS_BUTTON = True +RELEASE_BUTTON = False + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up Z-Wave Cover from Config Entry.""" + client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + + @callback + def async_add_cover(info: ZwaveDiscoveryInfo) -> None: + """Add Z-Wave cover.""" + entities: List[ZWaveBaseEntity] = [] + entities.append(ZWaveCover(config_entry, client, info)) + async_add_entities(entities) + + hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + async_dispatcher_connect( + hass, + f"{DOMAIN}_{config_entry.entry_id}_add_{COVER_DOMAIN}", + async_add_cover, + ) + ) + + +def percent_to_zwave_position(value: int) -> int: + """Convert position in 0-100 scale to 0-99 scale. + + `value` -- (int) Position byte value from 0-100. + """ + if value > 0: + return max(1, round((value / 100) * 99)) + return 0 + + +class ZWaveCover(ZWaveBaseEntity, CoverEntity): + """Representation of a Z-Wave Cover device.""" + + @property + def is_closed(self) -> bool: + """Return true if cover is closed.""" + return bool(self.info.primary_value.value == 0) + + @property + def current_cover_position(self) -> int: + """Return the current position of cover where 0 means closed and 100 is fully open.""" + return round((self.info.primary_value.value / 99) * 100) + + async def async_set_cover_position(self, **kwargs: Any) -> None: + """Move the cover to a specific position.""" + target_value = self.get_zwave_value("targetValue") + await self.info.node.async_set_value( + target_value, percent_to_zwave_position(kwargs[ATTR_POSITION]) + ) + + async def async_open_cover(self, **kwargs: Any) -> None: + """Open the cover.""" + target_value = self.get_zwave_value("Open") + await self.info.node.async_set_value(target_value, PRESS_BUTTON) + + async def async_close_cover(self, **kwargs: Any) -> None: + """Close cover.""" + target_value = self.get_zwave_value("Close") + await self.info.node.async_set_value(target_value, PRESS_BUTTON) + + async def async_stop_cover(self, **kwargs: Any) -> None: + """Stop cover.""" + target_value = self.get_zwave_value("Open") + await self.info.node.async_set_value(target_value, RELEASE_BUTTON) + target_value = self.get_zwave_value("Close") + await self.info.node.async_set_value(target_value, RELEASE_BUTTON) diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py new file mode 100644 index 00000000000..d741946a1c9 --- /dev/null +++ b/homeassistant/components/zwave_js/discovery.py @@ -0,0 +1,285 @@ +"""Map Z-Wave nodes and values to Home Assistant entities.""" + +from dataclasses import dataclass +from typing import Generator, Optional, Set, Union + +from zwave_js_server.const import CommandClass +from zwave_js_server.model.node import Node as ZwaveNode +from zwave_js_server.model.value import Value as ZwaveValue + +from homeassistant.core import callback + + +@dataclass +class ZwaveDiscoveryInfo: + """Info discovered from (primary) ZWave Value to create entity.""" + + node: ZwaveNode # node to which the value(s) belongs + primary_value: ZwaveValue # the value object itself for primary value + platform: str # the home assistant platform for which an entity should be created + platform_hint: Optional[ + str + ] = "" # hint for the platform about this discovered entity + + @property + def value_id(self) -> str: + """Return the unique value_id belonging to primary value.""" + return f"{self.node.node_id}.{self.primary_value.value_id}" + + +@dataclass +class ZWaveDiscoverySchema: + """Z-Wave discovery schema. + + The (primary) value for an entity must match these conditions. + Use the Z-Wave specifications to find out the values for these parameters: + https://github.com/zwave-js/node-zwave-js/tree/master/specs + """ + + # specify the hass platform for which this scheme applies (e.g. light, sensor) + platform: str + # [optional] hint for platform + hint: Optional[str] = None + # [optional] the node's basic device class must match ANY of these values + device_class_basic: Optional[Set[str]] = None + # [optional] the node's generic device class must match ANY of these values + device_class_generic: Optional[Set[str]] = None + # [optional] the node's specific device class must match ANY of these values + device_class_specific: Optional[Set[str]] = None + # [optional] the value's command class must match ANY of these values + command_class: Optional[Set[int]] = None + # [optional] the value's endpoint must match ANY of these values + endpoint: Optional[Set[int]] = None + # [optional] the value's property must match ANY of these values + property: Optional[Set[Union[str, int]]] = None + # [optional] the value's metadata_type must match ANY of these values + type: Optional[Set[str]] = None + + +# For device class mapping see: +# https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/deviceClasses.json +DISCOVERY_SCHEMAS = [ + # locks + ZWaveDiscoverySchema( + platform="lock", + device_class_generic={"Entry Control"}, + device_class_specific={ + "Door Lock", + "Advanced Door Lock", + "Secure Keypad Door Lock", + "Secure Lockbox", + }, + command_class={ + CommandClass.LOCK, + CommandClass.DOOR_LOCK, + }, + property={"currentMode", "locked"}, + type={"number", "boolean"}, + ), + # door lock door status + ZWaveDiscoverySchema( + platform="binary_sensor", + hint="property", + device_class_generic={"Entry Control"}, + device_class_specific={ + "Door Lock", + "Advanced Door Lock", + "Secure Keypad Door Lock", + "Secure Lockbox", + }, + command_class={ + CommandClass.LOCK, + CommandClass.DOOR_LOCK, + }, + property={"doorStatus"}, + type={"any"}, + ), + # climate + ZWaveDiscoverySchema( + platform="climate", + device_class_generic={"Thermostat"}, + device_class_specific={ + "Setback Thermostat", + "Thermostat General", + "Thermostat General V2", + }, + command_class={CommandClass.THERMOSTAT_MODE}, + property={"mode"}, + type={"number"}, + ), + # climate + # setpoint thermostats + ZWaveDiscoverySchema( + platform="climate", + device_class_generic={"Thermostat"}, + device_class_specific={ + "Setpoint Thermostat", + }, + command_class={CommandClass.THERMOSTAT_SETPOINT}, + property={"setpoint"}, + type={"number"}, + ), + # lights + # primary value is the currentValue (brightness) + ZWaveDiscoverySchema( + platform="light", + device_class_generic={"Multilevel Switch", "Remote Switch"}, + device_class_specific={ + "Tunable Color Light", + "Binary Tunable Color Light", + "Multilevel Remote Switch", + "Multilevel Power Switch", + "Multilevel Scene Switch", + }, + command_class={CommandClass.SWITCH_MULTILEVEL}, + property={"currentValue"}, + type={"number"}, + ), + # binary sensors + ZWaveDiscoverySchema( + platform="binary_sensor", + hint="boolean", + command_class={ + CommandClass.SENSOR_BINARY, + CommandClass.BATTERY, + }, + type={"boolean"}, + ), + ZWaveDiscoverySchema( + platform="binary_sensor", + hint="notification", + command_class={ + CommandClass.NOTIFICATION, + }, + type={"number"}, + ), + # generic text sensors + ZWaveDiscoverySchema( + platform="sensor", + hint="string_sensor", + command_class={ + CommandClass.SENSOR_ALARM, + CommandClass.INDICATOR, + }, + type={"string"}, + ), + # generic numeric sensors + ZWaveDiscoverySchema( + platform="sensor", + hint="numeric_sensor", + command_class={ + CommandClass.SENSOR_MULTILEVEL, + CommandClass.SENSOR_ALARM, + CommandClass.INDICATOR, + CommandClass.BATTERY, + }, + type={"number"}, + ), + # numeric sensors for Meter CC + ZWaveDiscoverySchema( + platform="sensor", + hint="numeric_sensor", + command_class={ + CommandClass.METER, + }, + type={"number"}, + property={"value"}, + ), + # special list sensors (Notification CC) + ZWaveDiscoverySchema( + platform="sensor", + hint="list_sensor", + command_class={ + CommandClass.NOTIFICATION, + }, + type={"number"}, + ), + # sensor for basic CC + ZWaveDiscoverySchema( + platform="sensor", + hint="numeric_sensor", + command_class={ + CommandClass.BASIC, + }, + type={"number"}, + property={"currentValue"}, + ), + # binary switches + ZWaveDiscoverySchema( + platform="switch", + command_class={CommandClass.SWITCH_BINARY}, + property={"currentValue"}, + ), + # cover + ZWaveDiscoverySchema( + platform="cover", + hint="cover", + device_class_generic={"Multilevel Switch"}, + device_class_specific={ + "Motor Control Class A", + "Motor Control Class B", + "Motor Control Class C", + "Multiposition Motor", + }, + command_class={CommandClass.SWITCH_MULTILEVEL}, + property={"currentValue"}, + type={"number"}, + ), + # fan + ZWaveDiscoverySchema( + platform="fan", + hint="fan", + device_class_generic={"Multilevel Switch"}, + device_class_specific={"Fan Switch"}, + command_class={CommandClass.SWITCH_MULTILEVEL}, + property={"currentValue"}, + type={"number"}, + ), +] + + +@callback +def async_discover_values(node: ZwaveNode) -> Generator[ZwaveDiscoveryInfo, None, None]: + """Run discovery on ZWave node and return matching (primary) values.""" + for value in node.values.values(): + for schema in DISCOVERY_SCHEMAS: + # check device_class_basic + if ( + schema.device_class_basic is not None + and value.node.device_class.basic not in schema.device_class_basic + ): + continue + # check device_class_generic + if ( + schema.device_class_generic is not None + and value.node.device_class.generic not in schema.device_class_generic + ): + continue + # check device_class_specific + if ( + schema.device_class_specific is not None + and value.node.device_class.specific not in schema.device_class_specific + ): + continue + # check command_class + if ( + schema.command_class is not None + and value.command_class not in schema.command_class + ): + continue + # check endpoint + if schema.endpoint is not None and value.endpoint not in schema.endpoint: + continue + # check property + if schema.property is not None and value.property_ not in schema.property: + continue + # check metadata_type + if schema.type is not None and value.metadata.type not in schema.type: + continue + # all checks passed, this value belongs to an entity + yield ZwaveDiscoveryInfo( + node=value.node, + primary_value=value, + platform=schema.platform, + platform_hint=schema.hint, + ) diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py new file mode 100644 index 00000000000..334a2cccd4f --- /dev/null +++ b/homeassistant/components/zwave_js/entity.py @@ -0,0 +1,174 @@ +"""Generic Z-Wave Entity Class.""" + +import logging +from typing import Optional, Tuple, Union + +from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.model.node import Node as ZwaveNode +from zwave_js_server.model.value import Value as ZwaveValue, get_value_id + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import callback +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN +from .discovery import ZwaveDiscoveryInfo + +LOGGER = logging.getLogger(__name__) + +EVENT_VALUE_UPDATED = "value updated" + + +@callback +def get_device_id(client: ZwaveClient, node: ZwaveNode) -> Tuple[str, str]: + """Get device registry identifier for Z-Wave node.""" + return (DOMAIN, f"{client.driver.controller.home_id}-{node.node_id}") + + +class ZWaveBaseEntity(Entity): + """Generic Entity Class for a Z-Wave Device.""" + + def __init__( + self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + ) -> None: + """Initialize a generic Z-Wave device entity.""" + self.config_entry = config_entry + self.client = client + self.info = info + # entities requiring additional values, can add extra ids to this list + self.watched_value_ids = {self.info.primary_value.value_id} + + @callback + def on_value_update(self) -> None: + """Call when one of the watched values change. + + To be overridden by platforms needing this event. + """ + + async def async_added_to_hass(self) -> None: + """Call when entity is added.""" + assert self.hass # typing + # Add value_changed callbacks. + self.async_on_remove( + self.info.node.on(EVENT_VALUE_UPDATED, self._value_changed) + ) + + @property + def device_info(self) -> dict: + """Return device information for the device registry.""" + # device is precreated in main handler + return { + "identifiers": {get_device_id(self.client, self.info.node)}, + } + + @property + def name(self) -> str: + """Return default name from device name and value name combination.""" + node_name = self.info.node.name or self.info.node.device_config.description + value_name = ( + self.info.primary_value.metadata.label + or self.info.primary_value.property_key_name + or self.info.primary_value.property_name + ) + # append endpoint if > 1 + if self.info.primary_value.endpoint > 1: + value_name += f" ({self.info.primary_value.endpoint})" + return f"{node_name}: {value_name}" + + @property + def unique_id(self) -> str: + """Return the unique_id of the entity.""" + return f"{self.client.driver.controller.home_id}.{self.info.value_id}" + + @property + def available(self) -> bool: + """Return entity availability.""" + return ( + self.client.connected + and bool(self.info.node.ready) + # a None value indicates something wrong with the device, + # or the value is simply not yet there (it will arrive later). + and self.info.primary_value.value is not None + ) + + @callback + def _value_changed(self, event_data: dict) -> None: + """Call when (one of) our watched values changes. + + Should not be overridden by subclasses. + """ + value_id = event_data["value"].value_id + + if value_id not in self.watched_value_ids: + return + + value = self.info.node.values[value_id] + + LOGGER.debug( + "[%s] Value %s/%s changed to: %s", + self.entity_id, + value.property_, + value.property_key_name, + value.value, + ) + + self.on_value_update() + self.async_write_ha_state() + + @callback + def get_zwave_value( + self, + value_property: Union[str, int], + command_class: Optional[int] = None, + endpoint: Optional[int] = None, + value_property_key_name: Optional[str] = None, + add_to_watched_value_ids: bool = True, + check_all_endpoints: bool = False, + ) -> Optional[ZwaveValue]: + """Return specific ZwaveValue on this ZwaveNode.""" + # use commandclass and endpoint from primary value if omitted + return_value = None + if command_class is None: + command_class = self.info.primary_value.command_class + if endpoint is None: + endpoint = self.info.primary_value.endpoint + + # Build partial event data dictionary so we can change the endpoint later + partial_evt_data = { + "commandClass": command_class, + "property": value_property, + "propertyKeyName": value_property_key_name, + } + + # lookup value by value_id + value_id = get_value_id( + self.info.node, {**partial_evt_data, "endpoint": endpoint} + ) + return_value = self.info.node.values.get(value_id) + + # If we haven't found a value and check_all_endpoints is True, we should + # return the first value we can find on any other endpoint + if return_value is None and check_all_endpoints: + for endpoint_ in self.info.node.endpoints: + if endpoint_.index != self.info.primary_value.endpoint: + value_id = get_value_id( + self.info.node, + {**partial_evt_data, "endpoint": endpoint_.index}, + ) + return_value = self.info.node.values.get(value_id) + if return_value: + break + + # add to watched_ids list so we will be triggered when the value updates + if ( + return_value + and return_value.value_id not in self.watched_value_ids + and add_to_watched_value_ids + ): + self.watched_value_ids.add(return_value.value_id) + return return_value + + @property + def should_poll(self) -> bool: + """No polling needed.""" + return False diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py new file mode 100644 index 00000000000..7113272d2ea --- /dev/null +++ b/homeassistant/components/zwave_js/fan.py @@ -0,0 +1,112 @@ +"""Support for Z-Wave fans.""" +import logging +import math +from typing import Any, Callable, List, Optional + +from zwave_js_server.client import Client as ZwaveClient + +from homeassistant.components.fan import ( + DOMAIN as FAN_DOMAIN, + SPEED_HIGH, + SPEED_LOW, + SPEED_MEDIUM, + SPEED_OFF, + SUPPORT_SET_SPEED, + FanEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN +from .discovery import ZwaveDiscoveryInfo +from .entity import ZWaveBaseEntity + +_LOGGER = logging.getLogger(__name__) + +SUPPORTED_FEATURES = SUPPORT_SET_SPEED + +# Value will first be divided to an integer +VALUE_TO_SPEED = {0: SPEED_OFF, 1: SPEED_LOW, 2: SPEED_MEDIUM, 3: SPEED_HIGH} +SPEED_TO_VALUE = {SPEED_OFF: 0, SPEED_LOW: 1, SPEED_MEDIUM: 50, SPEED_HIGH: 99} +SPEED_LIST = [*SPEED_TO_VALUE] + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up Z-Wave Fan from Config Entry.""" + client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + + @callback + def async_add_fan(info: ZwaveDiscoveryInfo) -> None: + """Add Z-Wave fan.""" + entities: List[ZWaveBaseEntity] = [] + entities.append(ZwaveFan(config_entry, client, info)) + async_add_entities(entities) + + hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + async_dispatcher_connect( + hass, + f"{DOMAIN}_{config_entry.entry_id}_add_{FAN_DOMAIN}", + async_add_fan, + ) + ) + + +class ZwaveFan(ZWaveBaseEntity, FanEntity): + """Representation of a Z-Wave fan.""" + + def __init__( + self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + ) -> None: + """Initialize the fan.""" + super().__init__(config_entry, client, info) + self._previous_speed: Optional[str] = None + + async def async_set_speed(self, speed: str) -> None: + """Set the speed of the fan.""" + if speed not in SPEED_TO_VALUE: + raise ValueError(f"Invalid speed received: {speed}") + self._previous_speed = speed + target_value = self.get_zwave_value("targetValue") + await self.info.node.async_set_value(target_value, SPEED_TO_VALUE[speed]) + + async def async_turn_on(self, speed: Optional[str] = None, **kwargs: Any) -> None: + """Turn the device on.""" + if speed is None: + # Value 255 tells device to return to previous value + target_value = self.get_zwave_value("targetValue") + await self.info.node.async_set_value(target_value, 255) + else: + await self.async_set_speed(speed) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the device off.""" + target_value = self.get_zwave_value("targetValue") + await self.info.node.async_set_value(target_value, 0) + + @property + def is_on(self) -> bool: + """Return true if device is on (speed above 0).""" + return bool(self.info.primary_value.value > 0) + + @property + def speed(self) -> Optional[str]: + """Return the current speed. + + The Z-Wave speed value is a byte 0-255. 255 means previous value. + The normal range of the speed is 0-99. 0 means off. + """ + value = math.ceil(self.info.primary_value.value * 3 / 100) + return VALUE_TO_SPEED.get(value, self._previous_speed) + + @property + def speed_list(self) -> List[str]: + """Get the list of available speeds.""" + return SPEED_LIST + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return SUPPORTED_FEATURES diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py new file mode 100644 index 00000000000..dd444fdb40d --- /dev/null +++ b/homeassistant/components/zwave_js/light.py @@ -0,0 +1,329 @@ +"""Support for Z-Wave lights.""" +import logging +from typing import Any, Callable, Optional, Tuple + +from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.const import CommandClass + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + ATTR_TRANSITION, + ATTR_WHITE_VALUE, + DOMAIN as LIGHT_DOMAIN, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, + SUPPORT_TRANSITION, + SUPPORT_WHITE_VALUE, + LightEntity, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +import homeassistant.util.color as color_util + +from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN +from .discovery import ZwaveDiscoveryInfo +from .entity import ZWaveBaseEntity + +LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up Z-Wave Light from Config Entry.""" + client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + + @callback + def async_add_light(info: ZwaveDiscoveryInfo) -> None: + """Add Z-Wave Light.""" + + light = ZwaveLight(config_entry, client, info) + async_add_entities([light]) + + hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + async_dispatcher_connect( + hass, + f"{DOMAIN}_{config_entry.entry_id}_add_{LIGHT_DOMAIN}", + async_add_light, + ) + ) + + +def byte_to_zwave_brightness(value: int) -> int: + """Convert brightness in 0-255 scale to 0-99 scale. + + `value` -- (int) Brightness byte value from 0-255. + """ + if value > 0: + return max(1, round((value / 255) * 99)) + return 0 + + +class ZwaveLight(ZWaveBaseEntity, LightEntity): + """Representation of a Z-Wave light.""" + + def __init__( + self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + ) -> None: + """Initialize the light.""" + super().__init__(config_entry, client, info) + self._supports_color = False + self._supports_white_value = False + self._supports_color_temp = False + self._hs_color: Optional[Tuple[float, float]] = None + self._white_value: Optional[int] = None + self._color_temp: Optional[int] = None + self._min_mireds = 153 # 6500K as a safe default + self._max_mireds = 370 # 2700K as a safe default + self._supported_features = SUPPORT_BRIGHTNESS + + # get additional (optional) values and set features + self._target_value = self.get_zwave_value("targetValue") + self._dimming_duration = self.get_zwave_value("duration") + if self._dimming_duration is not None: + self._supported_features |= SUPPORT_TRANSITION + self._calculate_color_values() + if self._supports_color: + self._supported_features |= SUPPORT_COLOR + if self._supports_color_temp: + self._supported_features |= SUPPORT_COLOR_TEMP + if self._supports_white_value: + self._supported_features |= SUPPORT_WHITE_VALUE + + @callback + def on_value_update(self) -> None: + """Call when a watched value is added or updated.""" + self._calculate_color_values() + + @property + def brightness(self) -> int: + """Return the brightness of this light between 0..255. + + Z-Wave multilevel switches use a range of [0, 99] to control brightness. + """ + if self.info.primary_value.value is not None: + return round((self.info.primary_value.value / 99) * 255) + return 0 + + @property + def is_on(self) -> bool: + """Return true if device is on (brightness above 0).""" + return self.brightness > 0 + + @property + def hs_color(self) -> Optional[Tuple[float, float]]: + """Return the hs color.""" + return self._hs_color + + @property + def white_value(self) -> Optional[int]: + """Return the white value of this light between 0..255.""" + return self._white_value + + @property + def color_temp(self) -> Optional[int]: + """Return the color temperature.""" + return self._color_temp + + @property + def min_mireds(self) -> int: + """Return the coldest color_temp that this light supports.""" + return self._min_mireds + + @property + def max_mireds(self) -> int: + """Return the warmest color_temp that this light supports.""" + return self._max_mireds + + @property + def supported_features(self) -> Optional[int]: + """Flag supported features.""" + return self._supported_features + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the device on.""" + # RGB/HS color + hs_color = kwargs.get(ATTR_HS_COLOR) + if hs_color is not None and self._supports_color: + # set white levels to 0 when setting rgb + await self._async_set_color("Warm White", 0) + await self._async_set_color("Cold White", 0) + red, green, blue = color_util.color_hs_to_RGB(*hs_color) + await self._async_set_color("Red", red) + await self._async_set_color("Green", green) + await self._async_set_color("Blue", blue) + + # Color temperature + color_temp = kwargs.get(ATTR_COLOR_TEMP) + if color_temp is not None and self._supports_color_temp: + # turn off rgb when setting white values + await self._async_set_color("Red", 0) + await self._async_set_color("Green", 0) + await self._async_set_color("Blue", 0) + # Limit color temp to min/max values + cold = max( + 0, + min( + 255, + round( + (self._max_mireds - color_temp) + / (self._max_mireds - self._min_mireds) + * 255 + ), + ), + ) + warm = 255 - cold + await self._async_set_color("Warm White", warm) + await self._async_set_color("Cold White", cold) + + # White value + white_value = kwargs.get(ATTR_WHITE_VALUE) + if white_value is not None and self._supports_white_value: + # turn off rgb when setting white values + await self._async_set_color("Red", 0) + await self._async_set_color("Green", 0) + await self._async_set_color("Blue", 0) + await self._async_set_color("Warm White", white_value) + + # set brightness + await self._async_set_brightness( + kwargs.get(ATTR_BRIGHTNESS), kwargs.get(ATTR_TRANSITION) + ) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the light off.""" + await self._async_set_brightness(0, kwargs.get(ATTR_TRANSITION)) + + async def _async_set_color(self, color_name: str, new_value: int) -> None: + """Set defined color to given value.""" + cur_zwave_value = self.get_zwave_value( + "currentColor", + CommandClass.SWITCH_COLOR, + value_property_key_name=color_name, + ) + # guard for unsupported command + if cur_zwave_value is None: + return + # actually set the new color value + target_zwave_value = self.get_zwave_value( + "targetColor", + CommandClass.SWITCH_COLOR, + value_property_key_name=color_name, + ) + if target_zwave_value is None: + return + await self.info.node.async_set_value(target_zwave_value, new_value) + + async def _async_set_brightness( + self, brightness: Optional[int], transition: Optional[int] = None + ) -> None: + """Set new brightness to light.""" + if brightness is None and self.info.primary_value.value: + # there is no point in setting default brightness when light is already on + return + if brightness is None: + # Level 255 means to set it to previous value. + zwave_brightness = 255 + else: + # Zwave multilevel switches use a range of [0, 99] to control brightness. + zwave_brightness = byte_to_zwave_brightness(brightness) + + # set transition value before sending new brightness + await self._async_set_transition_duration(transition) + # setting a value requires setting targetValue + await self.info.node.async_set_value(self._target_value, zwave_brightness) + + async def _async_set_transition_duration( + self, duration: Optional[int] = None + ) -> None: + """Set the transition time for the brightness value.""" + if self._dimming_duration is None: + return + # pylint: disable=fixme,unreachable + # TODO: setting duration needs to be fixed upstream + # https://github.com/zwave-js/node-zwave-js/issues/1321 + return + + if duration is None: # type: ignore + # no transition specified by user, use defaults + duration = 7621 # anything over 7620 uses the factory default + else: # pragma: no cover + # transition specified by user + transition = duration + if transition <= 127: + duration = transition + else: + minutes = round(transition / 60) + LOGGER.debug( + "Transition rounded to %d minutes for %s", + minutes, + self.entity_id, + ) + duration = minutes + 128 + + # only send value if it differs from current + # this prevents sending a command for nothing + if self._dimming_duration.value != duration: # pragma: no cover + await self.info.node.async_set_value(self._dimming_duration, duration) + + @callback + def _calculate_color_values(self) -> None: + """Calculate light colors.""" + + # RGB support + red_val = self.get_zwave_value( + "currentColor", CommandClass.SWITCH_COLOR, value_property_key_name="Red" + ) + green_val = self.get_zwave_value( + "currentColor", CommandClass.SWITCH_COLOR, value_property_key_name="Green" + ) + blue_val = self.get_zwave_value( + "currentColor", CommandClass.SWITCH_COLOR, value_property_key_name="Blue" + ) + if red_val and green_val and blue_val: + self._supports_color = True + # convert to HS + if ( + red_val.value is not None + and green_val.value is not None + and blue_val.value is not None + ): + self._hs_color = color_util.color_RGB_to_hs( + red_val.value, green_val.value, blue_val.value + ) + + # White colors + ww_val = self.get_zwave_value( + "currentColor", + CommandClass.SWITCH_COLOR, + value_property_key_name="Warm White", + ) + cw_val = self.get_zwave_value( + "currentColor", + CommandClass.SWITCH_COLOR, + value_property_key_name="Cold White", + ) + if ww_val and cw_val: + # Color temperature (CW + WW) Support + self._supports_color_temp = True + # Calculate color temps based on whites + cold_level = cw_val.value or 0 + if cold_level or ww_val.value is not None: + self._color_temp = round( + self._max_mireds + - ((cold_level / 255) * (self._max_mireds - self._min_mireds)) + ) + else: + self._color_temp = None + elif ww_val: + # only one white channel (warm white) + self._supports_white_value = True + self._white_value = ww_val.value + elif cw_val: + # only one white channel (cool white) + self._supports_white_value = True + self._white_value = cw_val.value diff --git a/homeassistant/components/zwave_js/lock.py b/homeassistant/components/zwave_js/lock.py new file mode 100644 index 00000000000..dedaf9a5e45 --- /dev/null +++ b/homeassistant/components/zwave_js/lock.py @@ -0,0 +1,128 @@ +"""Representation of Z-Wave locks.""" +import logging +from typing import Any, Callable, Dict, List, Optional, Union + +import voluptuous as vol +from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.const import ( + ATTR_CODE_SLOT, + ATTR_USERCODE, + LOCK_CMD_CLASS_TO_LOCKED_STATE_MAP, + LOCK_CMD_CLASS_TO_PROPERTY_MAP, + CommandClass, + DoorLockMode, +) +from zwave_js_server.model.value import Value as ZwaveValue +from zwave_js_server.util.lock import clear_usercode, set_usercode + +from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN, LockEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN +from .discovery import ZwaveDiscoveryInfo +from .entity import ZWaveBaseEntity + +LOGGER = logging.getLogger(__name__) + +STATE_TO_ZWAVE_MAP: Dict[int, Dict[str, Union[int, bool]]] = { + CommandClass.DOOR_LOCK: { + STATE_UNLOCKED: DoorLockMode.UNSECURED, + STATE_LOCKED: DoorLockMode.SECURED, + }, + CommandClass.LOCK: { + STATE_UNLOCKED: False, + STATE_LOCKED: True, + }, +} + +SERVICE_SET_LOCK_USERCODE = "set_lock_usercode" +SERVICE_CLEAR_LOCK_USERCODE = "clear_lock_usercode" + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up Z-Wave lock from config entry.""" + client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + + @callback + def async_add_lock(info: ZwaveDiscoveryInfo) -> None: + """Add Z-Wave Lock.""" + entities: List[ZWaveBaseEntity] = [] + entities.append(ZWaveLock(config_entry, client, info)) + + async_add_entities(entities) + + hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + async_dispatcher_connect( + hass, f"{DOMAIN}_{config_entry.entry_id}_add_{LOCK_DOMAIN}", async_add_lock + ) + ) + + platform = entity_platform.current_platform.get() + assert platform + + platform.async_register_entity_service( # type: ignore + SERVICE_SET_LOCK_USERCODE, + { + vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), + vol.Required(ATTR_USERCODE): cv.string, + }, + "async_set_lock_usercode", + ) + + platform.async_register_entity_service( # type: ignore + SERVICE_CLEAR_LOCK_USERCODE, + { + vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), + }, + "async_clear_lock_usercode", + ) + + +class ZWaveLock(ZWaveBaseEntity, LockEntity): + """Representation of a Z-Wave lock.""" + + @property + def is_locked(self) -> Optional[bool]: + """Return true if the lock is locked.""" + return int( + LOCK_CMD_CLASS_TO_LOCKED_STATE_MAP[ + CommandClass(self.info.primary_value.command_class) + ] + ) == int(self.info.primary_value.value) + + async def _set_lock_state( + self, target_state: str, **kwargs: Dict[str, Any] + ) -> None: + """Set the lock state.""" + target_value: ZwaveValue = self.get_zwave_value( + LOCK_CMD_CLASS_TO_PROPERTY_MAP[self.info.primary_value.command_class] + ) + if target_value is not None: + await self.info.node.async_set_value( + target_value, + STATE_TO_ZWAVE_MAP[self.info.primary_value.command_class][target_state], + ) + + async def async_lock(self, **kwargs: Dict[str, Any]) -> None: + """Lock the lock.""" + await self._set_lock_state(STATE_LOCKED) + + async def async_unlock(self, **kwargs: Dict[str, Any]) -> None: + """Unlock the lock.""" + await self._set_lock_state(STATE_UNLOCKED) + + async def async_set_lock_usercode(self, code_slot: int, usercode: str) -> None: + """Set the usercode to index X on the lock.""" + await set_usercode(self.info.node, code_slot, usercode) + LOGGER.debug("User code at slot %s set", code_slot) + + async def async_clear_lock_usercode(self, code_slot: int) -> None: + """Clear the usercode at index X on the lock.""" + await clear_usercode(self.info.node, code_slot) + LOGGER.debug("User code at slot %s cleared", code_slot) diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json new file mode 100644 index 00000000000..7df75d7aed2 --- /dev/null +++ b/homeassistant/components/zwave_js/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "zwave_js", + "name": "Z-Wave JS", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/zwave_js", + "requirements": ["zwave-js-server-python==0.17.0"], + "codeowners": ["@home-assistant/z-wave"], + "dependencies": ["http", "websocket_api"] +} diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py new file mode 100644 index 00000000000..3d3f782bc1b --- /dev/null +++ b/homeassistant/components/zwave_js/sensor.py @@ -0,0 +1,184 @@ +"""Representation of Z-Wave sensors.""" + +import logging +from typing import Callable, Dict, List, Optional + +from zwave_js_server.client import Client as ZwaveClient +from zwave_js_server.const import CommandClass + +from homeassistant.components.sensor import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_POWER, + DOMAIN as SENSOR_DOMAIN, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN +from .discovery import ZwaveDiscoveryInfo +from .entity import ZWaveBaseEntity + +LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up Z-Wave sensor from config entry.""" + client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + + @callback + def async_add_sensor(info: ZwaveDiscoveryInfo) -> None: + """Add Z-Wave Sensor.""" + entities: List[ZWaveBaseEntity] = [] + + if info.platform_hint == "string_sensor": + entities.append(ZWaveStringSensor(config_entry, client, info)) + elif info.platform_hint == "numeric_sensor": + entities.append(ZWaveNumericSensor(config_entry, client, info)) + elif info.platform_hint == "list_sensor": + entities.append(ZWaveListSensor(config_entry, client, info)) + else: + LOGGER.warning( + "Sensor not implemented for %s/%s", + info.platform_hint, + info.primary_value.propertyname, + ) + return + + async_add_entities(entities) + + hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + async_dispatcher_connect( + hass, + f"{DOMAIN}_{config_entry.entry_id}_add_{SENSOR_DOMAIN}", + async_add_sensor, + ) + ) + + +class ZwaveSensorBase(ZWaveBaseEntity): + """Basic Representation of a Z-Wave sensor.""" + + @property + def device_class(self) -> Optional[str]: + """Return the device class of the sensor.""" + if self.info.primary_value.command_class == CommandClass.BATTERY: + return DEVICE_CLASS_BATTERY + if self.info.primary_value.command_class == CommandClass.METER: + if self.info.primary_value.metadata.unit == "kWh": + return DEVICE_CLASS_ENERGY + return DEVICE_CLASS_POWER + if ( + isinstance(self.info.primary_value.property_, str) + and "temperature" in self.info.primary_value.property_.lower() + ): + return DEVICE_CLASS_TEMPERATURE + if self.info.primary_value.metadata.unit == "W": + return DEVICE_CLASS_POWER + if self.info.primary_value.metadata.unit == "Lux": + return DEVICE_CLASS_ILLUMINANCE + return None + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + # We hide some of the more advanced sensors by default to not overwhelm users + if self.info.primary_value.command_class in [ + CommandClass.BASIC, + CommandClass.INDICATOR, + CommandClass.NOTIFICATION, + ]: + return False + return True + + @property + def force_update(self) -> bool: + """Force updates.""" + return True + + +class ZWaveStringSensor(ZwaveSensorBase): + """Representation of a Z-Wave String sensor.""" + + @property + def state(self) -> Optional[str]: + """Return state of the sensor.""" + if self.info.primary_value.value is None: + return None + return str(self.info.primary_value.value) + + @property + def unit_of_measurement(self) -> Optional[str]: + """Return unit of measurement the value is expressed in.""" + if self.info.primary_value.metadata.unit is None: + return None + return str(self.info.primary_value.metadata.unit) + + +class ZWaveNumericSensor(ZwaveSensorBase): + """Representation of a Z-Wave Numeric sensor.""" + + @property + def state(self) -> float: + """Return state of the sensor.""" + if self.info.primary_value.value is None: + return 0 + return round(float(self.info.primary_value.value), 2) + + @property + def unit_of_measurement(self) -> Optional[str]: + """Return unit of measurement the value is expressed in.""" + if self.info.primary_value.metadata.unit is None: + return None + if self.info.primary_value.metadata.unit == "C": + return TEMP_CELSIUS + if self.info.primary_value.metadata.unit == "F": + return TEMP_FAHRENHEIT + + return str(self.info.primary_value.metadata.unit) + + @property + def name(self) -> str: + """Return default name from device name and value name combination.""" + if self.info.primary_value.command_class == CommandClass.BASIC: + node_name = self.info.node.name or self.info.node.device_config.description + label = self.info.primary_value.command_class_name + return f"{node_name}: {label}" + return super().name + + +class ZWaveListSensor(ZwaveSensorBase): + """Representation of a Z-Wave Numeric sensor with multiple states.""" + + @property + def state(self) -> Optional[str]: + """Return state of the sensor.""" + if self.info.primary_value.value is None: + return None + if ( + not str(self.info.primary_value.value) + in self.info.primary_value.metadata.states + ): + return None + return str( + self.info.primary_value.metadata.states[str(self.info.primary_value.value)] + ) + + @property + def device_state_attributes(self) -> Optional[Dict[str, str]]: + """Return the device specific state attributes.""" + # add the value's int value as property for multi-value (list) items + return {"value": self.info.primary_value.value} + + @property + def name(self) -> str: + """Return default name from device name and value name combination.""" + node_name = self.info.node.name or self.info.node.device_config.description + prop_name = self.info.primary_value.property_name + prop_key_name = self.info.primary_value.property_key_name + return f"{node_name}: {prop_name} - {prop_key_name}" diff --git a/homeassistant/components/zwave_js/services.yaml b/homeassistant/components/zwave_js/services.yaml new file mode 100644 index 00000000000..cc81da7ed58 --- /dev/null +++ b/homeassistant/components/zwave_js/services.yaml @@ -0,0 +1,24 @@ +# Describes the format for available Z-Wave services + +clear_lock_usercode: + description: Clear a usercode from lock. + fields: + entity_id: + description: Lock entity_id. + example: lock.front_door_locked + code_slot: + description: Code slot to clear code from. + example: 1 + +set_lock_usercode: + description: Set a usercode to lock. + fields: + entity_id: + description: Lock entity_id. + example: lock.front_door_locked + code_slot: + description: Code slot to set the code. + example: 1 + usercode: + description: Code to set. + example: 1234 diff --git a/homeassistant/components/zwave_js/strings.json b/homeassistant/components/zwave_js/strings.json new file mode 100644 index 00000000000..212bef70889 --- /dev/null +++ b/homeassistant/components/zwave_js/strings.json @@ -0,0 +1,49 @@ +{ + "title": "Z-Wave JS", + "config": { + "step": { + "manual": { + "data": { + "url": "[%key:common::config_flow::data::url%]" + } + }, + "on_supervisor": { + "title": "Select connection method", + "description": "Do you want to use the Z-Wave JS Supervisor add-on?", + "data": { "use_addon": "Use the Z-Wave JS Supervisor add-on" } + }, + "install_addon": { + "title": "The Z-Wave JS add-on installation has started" + }, + "start_addon": { + "title": "Enter the Z-Wave JS add-on configuration", + "data": { + "usb_path": "[%key:common::config_flow::data::usb_path%]", + "network_key": "Network Key" + } + }, + "hassio_confirm": { + "title": "Set up Z-Wave JS integration with the Z-Wave JS add-on" + } + }, + "error": { + "addon_start_failed": "Failed to start the Z-Wave JS add-on. Check the configuration.", + "invalid_ws_url": "Invalid websocket URL", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]", + "addon_info_failed": "Failed to get Z-Wave JS add-on info.", + "addon_install_failed": "Failed to install the Z-Wave JS add-on.", + "addon_set_config_failed": "Failed to set Z-Wave JS configuration.", + "addon_get_discovery_info_failed": "Failed to get Z-Wave JS add-on discovery info.", + "addon_missing_discovery_info": "Missing Z-Wave JS add-on discovery info.", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "progress": { + "install_addon": "Please wait while the Z-Wave JS add-on installation finishes. This can take several minutes." + } + } +} diff --git a/homeassistant/components/zwave_js/switch.py b/homeassistant/components/zwave_js/switch.py new file mode 100644 index 00000000000..2060894684c --- /dev/null +++ b/homeassistant/components/zwave_js/switch.py @@ -0,0 +1,61 @@ +"""Representation of Z-Wave switches.""" + +import logging +from typing import Any, Callable, List + +from zwave_js_server.client import Client as ZwaveClient + +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN +from .discovery import ZwaveDiscoveryInfo +from .entity import ZWaveBaseEntity + +LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable +) -> None: + """Set up Z-Wave sensor from config entry.""" + client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] + + @callback + def async_add_switch(info: ZwaveDiscoveryInfo) -> None: + """Add Z-Wave Switch.""" + entities: List[ZWaveBaseEntity] = [] + entities.append(ZWaveSwitch(config_entry, client, info)) + + async_add_entities(entities) + + hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + async_dispatcher_connect( + hass, + f"{DOMAIN}_{config_entry.entry_id}_add_{SWITCH_DOMAIN}", + async_add_switch, + ) + ) + + +class ZWaveSwitch(ZWaveBaseEntity, SwitchEntity): + """Representation of a Z-Wave switch.""" + + @property + def is_on(self) -> bool: + """Return a boolean for the state of the switch.""" + return bool(self.info.primary_value.value) + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the switch on.""" + target_value = self.get_zwave_value("targetValue") + if target_value is not None: + await self.info.node.async_set_value(target_value, True) + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the switch off.""" + target_value = self.get_zwave_value("targetValue") + if target_value is not None: + await self.info.node.async_set_value(target_value, False) diff --git a/homeassistant/components/zwave_js/translations/ca.json b/homeassistant/components/zwave_js/translations/ca.json new file mode 100644 index 00000000000..93ec53a644e --- /dev/null +++ b/homeassistant/components/zwave_js/translations/ca.json @@ -0,0 +1,56 @@ +{ + "config": { + "abort": { + "addon_get_discovery_info_failed": "No s'ha pogut obtenir la informaci\u00f3 de descobriment del complement Z-Wave JS.", + "addon_info_failed": "No s'ha pogut obtenir la informaci\u00f3 del complement Z-Wave JS.", + "addon_install_failed": "No s'ha pogut instal\u00b7lar el complement Z-Wave JS.", + "addon_missing_discovery_info": "Falta la informaci\u00f3 de descobriment del complement Z-Wave JS.", + "addon_set_config_failed": "No s'ha pogut establir la configuraci\u00f3 de Z-Wave JS.", + "already_configured": "El dispositiu ja est\u00e0 configurat", + "already_in_progress": "El flux de configuraci\u00f3 ja est\u00e0 en curs", + "cannot_connect": "Ha fallat la connexi\u00f3" + }, + "error": { + "addon_start_failed": "No s'ha pogut iniciar el complement Z-Wave JS. Comprova la configuraci\u00f3.", + "cannot_connect": "Ha fallat la connexi\u00f3", + "invalid_ws_url": "URL del websocket inv\u00e0lid", + "unknown": "Error inesperat" + }, + "progress": { + "install_addon": "Espera mentre finalitza la instal\u00b7laci\u00f3 del complement Z-Wave JS. Pot tardar uns quants minuts." + }, + "step": { + "hassio_confirm": { + "title": "Configura la integraci\u00f3 Z-Wave JS mitjan\u00e7ant el complement Z-Wave JS" + }, + "install_addon": { + "title": "Ha comen\u00e7at la instal\u00b7laci\u00f3 del complement Z-Wave JS" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "Utilitza el complement Z-Wave JS Supervisor" + }, + "description": "Vols utilitzar el complement Supervisor de Z-Wave JS?", + "title": "Selecciona el m\u00e8tode de connexi\u00f3" + }, + "start_addon": { + "data": { + "network_key": "Clau de xarxa", + "usb_path": "Ruta del port USB del dispositiu" + }, + "title": "Introdueix la configuraci\u00f3 del complement Z-Wave JS" + }, + "user": { + "data": { + "url": "URL" + } + } + } + }, + "title": "Z-Wave JS" +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/cs.json b/homeassistant/components/zwave_js/translations/cs.json new file mode 100644 index 00000000000..96073b579ed --- /dev/null +++ b/homeassistant/components/zwave_js/translations/cs.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Za\u0159\u00edzen\u00ed je ji\u017e nastaveno", + "already_in_progress": "Konfigurace ji\u017e prob\u00edh\u00e1", + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "error": { + "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit", + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + }, + "step": { + "manual": { + "data": { + "url": "URL" + } + }, + "start_addon": { + "data": { + "usb_path": "Cesta k USB za\u0159\u00edzen\u00ed" + } + }, + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/de.json b/homeassistant/components/zwave_js/translations/de.json new file mode 100644 index 00000000000..d4903bc8c6d --- /dev/null +++ b/homeassistant/components/zwave_js/translations/de.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/en.json b/homeassistant/components/zwave_js/translations/en.json new file mode 100644 index 00000000000..977651a576b --- /dev/null +++ b/homeassistant/components/zwave_js/translations/en.json @@ -0,0 +1,56 @@ +{ + "config": { + "abort": { + "addon_get_discovery_info_failed": "Failed to get Z-Wave JS add-on discovery info.", + "addon_info_failed": "Failed to get Z-Wave JS add-on info.", + "addon_install_failed": "Failed to install the Z-Wave JS add-on.", + "addon_missing_discovery_info": "Missing Z-Wave JS add-on discovery info.", + "addon_set_config_failed": "Failed to set Z-Wave JS configuration.", + "already_configured": "Device is already configured", + "already_in_progress": "Configuration flow is already in progress", + "cannot_connect": "Failed to connect" + }, + "error": { + "addon_start_failed": "Failed to start the Z-Wave JS add-on. Check the configuration.", + "cannot_connect": "Failed to connect", + "invalid_ws_url": "Invalid websocket URL", + "unknown": "Unexpected error" + }, + "progress": { + "install_addon": "Please wait while the Z-Wave JS add-on installation finishes. This can take several minutes." + }, + "step": { + "hassio_confirm": { + "title": "Set up Z-Wave JS integration with the Z-Wave JS add-on" + }, + "install_addon": { + "title": "The Z-Wave JS add-on installation has started" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "Use the Z-Wave JS Supervisor add-on" + }, + "description": "Do you want to use the Z-Wave JS Supervisor add-on?", + "title": "Select connection method" + }, + "start_addon": { + "data": { + "network_key": "Network Key", + "usb_path": "USB Device Path" + }, + "title": "Enter the Z-Wave JS add-on configuration" + }, + "user": { + "data": { + "url": "URL" + } + } + } + }, + "title": "Z-Wave JS" +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/es.json b/homeassistant/components/zwave_js/translations/es.json new file mode 100644 index 00000000000..e5ee009c0d1 --- /dev/null +++ b/homeassistant/components/zwave_js/translations/es.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n ya est\u00e1 en proceso", + "cannot_connect": "No se pudo conectar" + }, + "error": { + "cannot_connect": "No se pudo conectar", + "invalid_ws_url": "URL de websocket no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "title": "Selecciona el m\u00e9todo de conexi\u00f3n" + }, + "start_addon": { + "data": { + "network_key": "Clave de red", + "usb_path": "Ruta del dispositivo USB" + } + }, + "user": { + "data": { + "url": "URL" + } + } + } + }, + "title": "Z-Wave JS" +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/et.json b/homeassistant/components/zwave_js/translations/et.json new file mode 100644 index 00000000000..7a7aadfb841 --- /dev/null +++ b/homeassistant/components/zwave_js/translations/et.json @@ -0,0 +1,56 @@ +{ + "config": { + "abort": { + "addon_get_discovery_info_failed": "Z-Wave JS lisandmooduli tuvastusteabe hankimine nurjus.", + "addon_info_failed": "Z-Wave JS lisandmooduli teabe hankimine nurjus.", + "addon_install_failed": "Z-Wave JS lisandmooduli paigaldamine nurjus.", + "addon_missing_discovery_info": "Z-Wave JS lisandmooduli tuvastusteave puudub.", + "addon_set_config_failed": "Z-Wave JS konfiguratsiooni m\u00e4\u00e4ramine nurjus.", + "already_configured": "Seade on juba h\u00e4\u00e4lestatud", + "already_in_progress": "Seadistamine on juba k\u00e4imas", + "cannot_connect": "\u00dchendamine nurjus" + }, + "error": { + "addon_start_failed": "Z-Wave JS lisandmooduli k\u00e4ivitamine nurjus. Kontrolli seadistusi.", + "cannot_connect": "\u00dchendamine nurjus", + "invalid_ws_url": "Vale sihtkoha aadress", + "unknown": "Ootamatu t\u00f5rge" + }, + "progress": { + "install_addon": "Palun oota kuni Z-Wave JS lisandmoodul on paigaldatud. See v\u00f5ib v\u00f5tta mitu minutit." + }, + "step": { + "hassio_confirm": { + "title": "Seadista Z-Wave JS-i sidumine Z-Wave JS-i lisandmooduliga" + }, + "install_addon": { + "title": "Z-Wave JS lisandmooduli paigaldamine on alanud" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "Kasuta lisandmoodulit Z-Wave JS Supervisor" + }, + "description": "Kas soovid kasutada Z-Wave JSi halduri lisandmoodulit?", + "title": "Vali \u00fchendusviis" + }, + "start_addon": { + "data": { + "network_key": "V\u00f5rgu v\u00f5ti", + "usb_path": "USB-seadme asukoha rada" + }, + "title": "Sisesta Z-Wave JS lisandmooduli seaded" + }, + "user": { + "data": { + "url": "" + } + } + } + }, + "title": "Z-Wave JS" +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/fr.json b/homeassistant/components/zwave_js/translations/fr.json new file mode 100644 index 00000000000..f3a9aff1a29 --- /dev/null +++ b/homeassistant/components/zwave_js/translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Le p\u00e9riph\u00e9rique est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "Erreur de connection", + "invalid_ws_url": "URL websocket invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + }, + "title": "Z-Wave JS" +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/it.json b/homeassistant/components/zwave_js/translations/it.json new file mode 100644 index 00000000000..fc76b309a34 --- /dev/null +++ b/homeassistant/components/zwave_js/translations/it.json @@ -0,0 +1,56 @@ +{ + "config": { + "abort": { + "addon_get_discovery_info_failed": "Impossibile ottenere le informazioni sul rilevamento del componente aggiuntivo Z-Wave JS.", + "addon_info_failed": "Impossibile ottenere le informazioni sul componente aggiuntivo Z-Wave JS.", + "addon_install_failed": "Impossibile installare il componente aggiuntivo Z-Wave JS.", + "addon_missing_discovery_info": "Informazioni sul rilevamento del componente aggiuntivo Z-Wave JS mancanti.", + "addon_set_config_failed": "Impossibile impostare la configurazione di Z-Wave JS.", + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", + "already_in_progress": "Il flusso di configurazione \u00e8 gi\u00e0 in corso", + "cannot_connect": "Impossibile connettersi" + }, + "error": { + "addon_start_failed": "Impossibile avviare il componente aggiuntivo Z-Wave JS. Controlla la configurazione.", + "cannot_connect": "Impossibile connettersi", + "invalid_ws_url": "URL websocket non valido", + "unknown": "Errore imprevisto" + }, + "progress": { + "install_addon": "Attendi il termine dell'installazione del componente aggiuntivo Z-Wave JS. Questa operazione pu\u00f2 richiedere diversi minuti." + }, + "step": { + "hassio_confirm": { + "title": "Configura l'integrazione di Z-Wave JS con il componente aggiuntivo Z-Wave JS" + }, + "install_addon": { + "title": "L'installazione del componente aggiuntivo Z-Wave JS \u00e8 iniziata" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "Usa il componente aggiuntivo Z-Wave JS Supervisor" + }, + "description": "Desideri utilizzare il componente aggiuntivo Z-Wave JS Supervisor?", + "title": "Seleziona il metodo di connessione" + }, + "start_addon": { + "data": { + "network_key": "Chiave di rete", + "usb_path": "Percorso del dispositivo USB" + }, + "title": "Accedi alla configurazione del componente aggiuntivo Z-Wave JS" + }, + "user": { + "data": { + "url": "URL" + } + } + } + }, + "title": "Z-Wave JS" +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/lb.json b/homeassistant/components/zwave_js/translations/lb.json new file mode 100644 index 00000000000..302addbd7cf --- /dev/null +++ b/homeassistant/components/zwave_js/translations/lb.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen", + "invalid_ws_url": "Ong\u00eblteg Websocket URL", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "url": "URL" + } + } + } + }, + "title": "Z-Wave JS" +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/no.json b/homeassistant/components/zwave_js/translations/no.json new file mode 100644 index 00000000000..e16425b59ec --- /dev/null +++ b/homeassistant/components/zwave_js/translations/no.json @@ -0,0 +1,56 @@ +{ + "config": { + "abort": { + "addon_get_discovery_info_failed": "Kunne ikke hente oppdagelsesinformasjon om Z-Wave JS-tillegg", + "addon_info_failed": "Kunne ikke hente informasjon om Z-Wave JS-tillegg", + "addon_install_failed": "Kunne ikke installere Z-Wave JS-tillegg", + "addon_missing_discovery_info": "Manglende oppdagelsesinformasjon for Z-Wave JS-tillegg", + "addon_set_config_failed": "Kunne ikke angi Z-Wave JS-konfigurasjon", + "already_configured": "Enheten er allerede konfigurert", + "already_in_progress": "Konfigurasjonsflyten p\u00e5g\u00e5r allerede", + "cannot_connect": "Tilkobling mislyktes" + }, + "error": { + "addon_start_failed": "Kunne ikke starte Z-Wave JS-tillegg. Sjekk konfigurasjonen.", + "cannot_connect": "Tilkobling mislyktes", + "invalid_ws_url": "Ugyldig websocket URL", + "unknown": "Uventet feil" + }, + "progress": { + "install_addon": "Vent mens installasjonen av Z-Wave JS-tillegg er ferdig. Dette kan ta flere minutter." + }, + "step": { + "hassio_confirm": { + "title": "Sett opp Z-Wave JS-integrasjon med Z-Wave JS-tillegg" + }, + "install_addon": { + "title": "Installasjon av Z-Wave JS-tillegg har startet" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "Bruk Z-Wave JS Supervisor-tillegg" + }, + "description": "Vil du bruke Z-Wave JS Supervisor-tillegg?", + "title": "Velg tilkoblingsmetode" + }, + "start_addon": { + "data": { + "network_key": "Nettverksn\u00f8kkel", + "usb_path": "USB enhetsbane" + }, + "title": "Angi konfigurasjon for Z-Wave JS-tillegg" + }, + "user": { + "data": { + "url": "URL" + } + } + } + }, + "title": "" +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/pl.json b/homeassistant/components/zwave_js/translations/pl.json new file mode 100644 index 00000000000..47e263c6101 --- /dev/null +++ b/homeassistant/components/zwave_js/translations/pl.json @@ -0,0 +1,56 @@ +{ + "config": { + "abort": { + "addon_get_discovery_info_failed": "Nie uda\u0142o si\u0119 uzyska\u0107 informacji wykrywania dodatku Z-Wave JS", + "addon_info_failed": "Nie uda\u0142o si\u0119 uzyska\u0107 informacji o dodatku Z-Wave JS", + "addon_install_failed": "Nie uda\u0142o si\u0119 zainstalowa\u0107 dodatku Z-Wave JS", + "addon_missing_discovery_info": "Brak informacji wykrywania dodatku Z-Wave JS", + "addon_set_config_failed": "Nie uda\u0142o si\u0119 skonfigurowa\u0107 Z-Wave JS", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", + "already_in_progress": "Konfiguracja jest ju\u017c w toku", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" + }, + "error": { + "addon_start_failed": "Nie uda\u0142o si\u0119 uruchomi\u0107 dodatku Z-Wave JS. Sprawd\u017a konfiguracj\u0119", + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia", + "invalid_ws_url": "Nieprawid\u0142owy URL websocket", + "unknown": "Nieoczekiwany b\u0142\u0105d" + }, + "progress": { + "install_addon": "Poczekaj, a\u017c zako\u0144czy si\u0119 instalacja dodatku Z-Wave JS. Mo\u017ce to zaj\u0105\u0107 kilka minut." + }, + "step": { + "hassio_confirm": { + "title": "Skonfiguruj integracj\u0119 Z-Wave JS z dodatkiem Z-Wave JS" + }, + "install_addon": { + "title": "Rozpocz\u0119\u0142a si\u0119 instalacja dodatku Z-Wave JS" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "U\u017cyj dodatku Z-Wave JS Supervisor" + }, + "description": "Czy chcesz skorzysta\u0107 z dodatku Z-Wave JS Supervisor?", + "title": "Wybierz metod\u0119 po\u0142\u0105czenia" + }, + "start_addon": { + "data": { + "network_key": "Klucz sieci", + "usb_path": "\u015acie\u017cka urz\u0105dzenia USB" + }, + "title": "Wprowad\u017a konfiguracj\u0119 dodatku Z-Wave JS" + }, + "user": { + "data": { + "url": "URL" + } + } + } + }, + "title": "Z-Wave JS" +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/pt-BR.json b/homeassistant/components/zwave_js/translations/pt-BR.json new file mode 100644 index 00000000000..e29d809ebff --- /dev/null +++ b/homeassistant/components/zwave_js/translations/pt-BR.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "already_configured": "Dispositivo j\u00e1 est\u00e1 configurado" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/ru.json b/homeassistant/components/zwave_js/translations/ru.json new file mode 100644 index 00000000000..2d9609e9d00 --- /dev/null +++ b/homeassistant/components/zwave_js/translations/ru.json @@ -0,0 +1,56 @@ +{ + "config": { + "abort": { + "addon_get_discovery_info_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e\u0431 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0438 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS.", + "addon_info_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0438 Z-Wave JS.", + "addon_install_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Z-Wave JS.", + "addon_missing_discovery_info": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0438 Z-Wave JS.", + "addon_set_config_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e Z-Wave JS.", + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", + "already_in_progress": "\u041f\u0440\u043e\u0446\u0435\u0441\u0441 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." + }, + "error": { + "addon_start_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Z-Wave JS. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_ws_url": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 URL-\u0430\u0434\u0440\u0435\u0441.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "progress": { + "install_addon": "\u041f\u043e\u0434\u043e\u0436\u0434\u0438\u0442\u0435, \u043f\u043e\u043a\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u0441\u044f \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS. \u042d\u0442\u043e \u043c\u043e\u0436\u0435\u0442 \u0437\u0430\u043d\u044f\u0442\u044c \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u043c\u0438\u043d\u0443\u0442." + }, + "step": { + "hassio_confirm": { + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 Z-Wave JS (\u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Home Assistant Z-Wave JS)" + }, + "install_addon": { + "title": "\u041d\u0430\u0447\u0430\u043b\u0430\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS" + }, + "manual": { + "data": { + "url": "URL-\u0430\u0434\u0440\u0435\u0441" + } + }, + "on_supervisor": { + "data": { + "use_addon": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Supervisor Z-Wave JS" + }, + "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Supervisor Z-Wave JS?", + "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f" + }, + "start_addon": { + "data": { + "network_key": "\u041a\u043b\u044e\u0447 \u0441\u0435\u0442\u0438", + "usb_path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + }, + "title": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS" + }, + "user": { + "data": { + "url": "URL-\u0430\u0434\u0440\u0435\u0441" + } + } + } + }, + "title": "Z-Wave JS" +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/tr.json b/homeassistant/components/zwave_js/translations/tr.json new file mode 100644 index 00000000000..2faa8ba4307 --- /dev/null +++ b/homeassistant/components/zwave_js/translations/tr.json @@ -0,0 +1,56 @@ +{ + "config": { + "abort": { + "addon_get_discovery_info_failed": "Z-Wave JS eklenti ke\u015fif bilgileri al\u0131namad\u0131.", + "addon_info_failed": "Z-Wave JS eklenti bilgileri al\u0131namad\u0131.", + "addon_install_failed": "Z-Wave JS eklentisi y\u00fcklenemedi.", + "addon_missing_discovery_info": "Eksik Z-Wave JS eklenti bulma bilgileri.", + "addon_set_config_failed": "Z-Wave JS yap\u0131land\u0131rmas\u0131 ayarlanamad\u0131.", + "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", + "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", + "cannot_connect": "Ba\u011flanma hatas\u0131" + }, + "error": { + "addon_start_failed": "Z-Wave JS eklentisi ba\u015flat\u0131lamad\u0131. Yap\u0131land\u0131rmay\u0131 kontrol edin.", + "cannot_connect": "Ba\u011flanma hatas\u0131", + "invalid_ws_url": "Ge\u00e7ersiz websocket URL'si", + "unknown": "Beklenmeyen hata" + }, + "progress": { + "install_addon": "L\u00fctfen Z-Wave JS eklenti kurulumu bitene kadar bekleyin. Bu birka\u00e7 dakika s\u00fcrebilir." + }, + "step": { + "hassio_confirm": { + "title": "Z-Wave JS eklentisiyle Z-Wave JS entegrasyonunu ayarlay\u0131n" + }, + "install_addon": { + "title": "Z-Wave JS eklenti kurulumu ba\u015flad\u0131" + }, + "manual": { + "data": { + "url": "URL" + } + }, + "on_supervisor": { + "data": { + "use_addon": "Z-Wave JS Supervisor eklentisini kullan\u0131n" + }, + "description": "Z-Wave JS Supervisor eklentisini kullanmak istiyor musunuz?", + "title": "Ba\u011flant\u0131 y\u00f6ntemini se\u00e7in" + }, + "start_addon": { + "data": { + "network_key": "A\u011f Anahtar\u0131", + "usb_path": "USB Ayg\u0131t Yolu" + }, + "title": "Z-Wave JS eklenti yap\u0131land\u0131rmas\u0131na girin" + }, + "user": { + "data": { + "url": "URL" + } + } + } + }, + "title": "Z-Wave JS" +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/uk.json b/homeassistant/components/zwave_js/translations/uk.json new file mode 100644 index 00000000000..f5ff5224347 --- /dev/null +++ b/homeassistant/components/zwave_js/translations/uk.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u0426\u0435\u0439 \u043f\u0440\u0438\u0441\u0442\u0440\u0456\u0439 \u0432\u0436\u0435 \u0434\u043e\u0434\u0430\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", + "invalid_ws_url": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0430 URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0432\u0435\u0431-\u0441\u043e\u043a\u0435\u0442\u0430", + "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" + }, + "step": { + "user": { + "data": { + "url": "URL-\u0430\u0434\u0440\u0435\u0441\u0430" + } + } + } + }, + "title": "Z-Wave JS" +} \ No newline at end of file diff --git a/homeassistant/components/zwave_js/translations/zh-Hant.json b/homeassistant/components/zwave_js/translations/zh-Hant.json new file mode 100644 index 00000000000..1cbde8f886b --- /dev/null +++ b/homeassistant/components/zwave_js/translations/zh-Hant.json @@ -0,0 +1,56 @@ +{ + "config": { + "abort": { + "addon_get_discovery_info_failed": "\u53d6\u5f97 Z-Wave JS add-on \u63a2\u7d22\u8cc7\u8a0a\u5931\u6557\u3002", + "addon_info_failed": "\u53d6\u5f97 Z-Wave JS add-on \u8cc7\u8a0a\u5931\u6557\u3002", + "addon_install_failed": "Z-Wave JS add-on \u5b89\u88dd\u5931\u6557\u3002", + "addon_missing_discovery_info": "\u7f3a\u5c11 Z-Wave JS add-on \u63a2\u7d22\u8cc7\u8a0a\u3002", + "addon_set_config_failed": "Z-Wave JS add-on \u8a2d\u5b9a\u5931\u6557\u3002", + "already_configured": "\u88dd\u7f6e\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_in_progress": "\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d", + "cannot_connect": "\u9023\u7dda\u5931\u6557" + }, + "error": { + "addon_start_failed": "Z-Wave JS add-on \u555f\u52d5\u5931\u6557\uff0c\u8acb\u6aa2\u67e5\u8a2d\u5b9a\u3002", + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_ws_url": "Websocket URL \u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "progress": { + "install_addon": "\u8acb\u7a0d\u7b49 Z-Wave JS add-on \u5b89\u88dd\u5b8c\u6210\uff0c\u53ef\u80fd\u6703\u9700\u8981\u5e7e\u5206\u9418\u3002" + }, + "step": { + "hassio_confirm": { + "title": "\u4ee5 Z-Wave JS add-on \u8a2d\u5b9a Z-Wave JS \u6574\u5408" + }, + "install_addon": { + "title": "Z-Wave JS add-on \u5b89\u88dd\u5df2\u555f\u52d5" + }, + "manual": { + "data": { + "url": "\u7db2\u5740" + } + }, + "on_supervisor": { + "data": { + "use_addon": "\u4f7f\u7528 Z-Wave JS Supervisor add-on" + }, + "description": "\u662f\u5426\u8981\u4f7f\u7528 Z-Wave JS Supervisor add-on\uff1f", + "title": "\u9078\u64c7\u9023\u7dda\u985e\u578b" + }, + "start_addon": { + "data": { + "network_key": "\u7db2\u8def\u5bc6\u9470", + "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" + }, + "title": "\u8f38\u5165 Z-Wave JS \u9644\u52a0\u8a2d\u5b9a" + }, + "user": { + "data": { + "url": "\u7db2\u5740" + } + } + } + }, + "title": "Z-Wave JS" +} \ No newline at end of file diff --git a/homeassistant/config.py b/homeassistant/config.py index c3fe18d69df..2da9b0331c9 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -412,17 +412,19 @@ def async_log_exception( """ if hass is not None: async_notify_setup_error(hass, domain, link) - _LOGGER.error(_format_config_error(ex, domain, config, link)) + message, is_friendly = _format_config_error(ex, domain, config, link) + _LOGGER.error(message, exc_info=not is_friendly and ex) @callback def _format_config_error( ex: Exception, domain: str, config: Dict, link: Optional[str] = None -) -> str: +) -> Tuple[str, bool]: """Generate log exception for configuration validation. This method must be run in the event loop. """ + is_friendly = False message = f"Invalid config for [{domain}]: " if isinstance(ex, vol.Invalid): if "extra keys not allowed" in ex.error_message: @@ -433,8 +435,9 @@ def _format_config_error( ) else: message += f"{humanize_error(config, ex)}." + is_friendly = True else: - message += str(ex) + message += str(ex) or repr(ex) try: domain_config = config.get(domain, config) @@ -449,7 +452,7 @@ def _format_config_error( if domain != CONF_CORE and link: message += f"Please check the docs at {link}" - return message + return message, is_friendly async def async_process_ha_core_config(hass: HomeAssistant, config: Dict) -> None: diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 601ce1efbfe..abc6b2f46af 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -2,7 +2,7 @@ import asyncio import functools import logging -from types import MappingProxyType +from types import MappingProxyType, MethodType from typing import Any, Callable, Dict, List, Optional, Set, Union, cast import weakref @@ -29,6 +29,7 @@ SOURCE_MQTT = "mqtt" SOURCE_SSDP = "ssdp" SOURCE_USER = "user" SOURCE_ZEROCONF = "zeroconf" +SOURCE_DHCP = "dhcp" # If a user wants to hide a discovery from the UI they can "Ignore" it. The config_entries/ignore_flow # websocket command creates a config entry with this source and while it exists normal discoveries @@ -180,7 +181,9 @@ class ConfigEntry: self.supports_unload = False # Listeners to call on update - self.update_listeners: List[weakref.ReferenceType[UpdateListenerType]] = [] + self.update_listeners: List[ + Union[weakref.ReferenceType[UpdateListenerType], weakref.WeakMethod] + ] = [] # Function to cancel a scheduled retry self._async_cancel_retry_setup: Optional[Callable[[], Any]] = None @@ -245,7 +248,8 @@ class ConfigEntry: wait_time = 2 ** min(tries, 4) * 5 tries += 1 _LOGGER.warning( - "Config entry for %s not ready yet. Retrying in %d seconds", + "Config entry '%s' for %s integration not ready yet. Retrying in %d seconds", + self.title, self.domain, wait_time, ) @@ -412,7 +416,12 @@ class ConfigEntry: Returns function to unlisten. """ - weak_listener = weakref.ref(listener) + weak_listener: Any + # weakref.ref is not applicable to a bound method, e.g. method of a class instance, as reference will die immediately + if hasattr(listener, "__self__"): + weak_listener = weakref.WeakMethod(cast(MethodType, listener)) + else: + weak_listener = weakref.ref(listener) self.update_listeners.append(weak_listener) return lambda: self.update_listeners.remove(weak_listener) @@ -975,7 +984,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): async def async_step_ignore(self, user_input: Dict[str, Any]) -> Dict[str, Any]: """Ignore this config flow.""" await self.async_set_unique_id(user_input["unique_id"], raise_on_progress=False) - return self.async_create_entry(title="Ignored", data={}) + return self.async_create_entry(title=user_input["title"], data={}) async def async_step_unignore(self, user_input: Dict[str, Any]) -> Dict[str, Any]: """Rediscover a config entry by it's unique_id.""" @@ -1045,6 +1054,7 @@ class ConfigFlow(data_entry_flow.FlowHandler): async_step_mqtt = async_step_discovery async_step_ssdp = async_step_discovery async_step_zeroconf = async_step_discovery + async_step_dhcp = async_step_discovery class OptionsFlowManager(data_entry_flow.FlowManager): diff --git a/homeassistant/const.py b/homeassistant/const.py index f7e795a281e..bab77c63a94 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,13 +1,13 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 2021 -MINOR_VERSION = 1 -PATCH_VERSION = "5" +MINOR_VERSION = 2 +PATCH_VERSION = "0b5" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" -REQUIRED_PYTHON_VER = (3, 7, 1) +REQUIRED_PYTHON_VER = (3, 8, 0) # Truthy date string triggers showing related deprecation warning messages. -REQUIRED_NEXT_PYTHON_VER = (3, 8, 0) -REQUIRED_NEXT_PYTHON_DATE = "December 7, 2020" +REQUIRED_NEXT_PYTHON_VER = (3, 9, 0) +REQUIRED_NEXT_PYTHON_DATE = "" # Format for platform files PLATFORM_FORMAT = "{platform}.{domain}" diff --git a/homeassistant/core.py b/homeassistant/core.py index 6b657f600d8..dfdb77a44a8 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -71,9 +71,12 @@ from homeassistant.exceptions import ( Unauthorized, ) from homeassistant.util import location, network -from homeassistant.util.async_ import fire_coroutine_threadsafe, run_callback_threadsafe +from homeassistant.util.async_ import ( + fire_coroutine_threadsafe, + run_callback_threadsafe, + shutdown_run_callback_threadsafe, +) import homeassistant.util.dt as dt_util -from homeassistant.util.thread import fix_threading_exception_logging from homeassistant.util.timeout import TimeoutManager from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem import homeassistant.util.uuid as uuid_util @@ -86,7 +89,6 @@ if TYPE_CHECKING: block_async_io.enable() -fix_threading_exception_logging() T = TypeVar("T") _UNDEF: dict = {} # Internal; not helpers.typing.UNDEFINED due to circular dependency @@ -118,7 +120,7 @@ _LOGGER = logging.getLogger(__name__) def split_entity_id(entity_id: str) -> List[str]: - """Split a state entity_id into domain, object_id.""" + """Split a state entity ID into domain and object ID.""" return entity_id.split(".", 1) @@ -306,7 +308,7 @@ class HomeAssistant: _LOGGER.warning( "Something is blocking Home Assistant from wrapping up the " "start up phase. We're going to continue anyway. Please " - "report the following info at http://bit.ly/2ogP58T : %s", + "report the following info at https://github.com/home-assistant/core/issues: %s", ", ".join(self.config.components), ) @@ -550,6 +552,14 @@ class HomeAssistant: # stage 3 self.state = CoreState.not_running self.bus.async_fire(EVENT_HOMEASSISTANT_CLOSE) + + # Prevent run_callback_threadsafe from scheduling any additional + # callbacks in the event loop as callbacks created on the futures + # it returns will never run after the final `self.async_block_till_done` + # which will cause the futures to block forever when waiting for + # the `result()` which will cause a deadlock when shutting down the executor. + shutdown_run_callback_threadsafe(self.loop) + try: async with self.timeout.async_timeout(30): await self.async_block_till_done() @@ -573,7 +583,7 @@ class Context: parent_id: Optional[str] = attr.ib(default=None) id: str = attr.ib(factory=uuid_util.random_uuid_hex) - def as_dict(self) -> dict: + def as_dict(self) -> Dict[str, Optional[str]]: """Return a dictionary representation of the context.""" return {"id": self.id, "parent_id": self.parent_id, "user_id": self.user_id} @@ -614,7 +624,7 @@ class Event: # The only event type that shares context are the TIME_CHANGED return hash((self.event_type, self.context.id, self.time_fired)) - def as_dict(self) -> Dict: + def as_dict(self) -> Dict[str, Any]: """Create a dict representation of this Event. Async friendly. @@ -684,7 +694,7 @@ class EventBus: def async_fire( self, event_type: str, - event_data: Optional[Dict] = None, + event_data: Optional[Dict[str, Any]] = None, origin: EventOrigin = EventOrigin.local, context: Optional[Context] = None, time_fired: Optional[datetime.datetime] = None, @@ -846,7 +856,7 @@ class State: self, entity_id: str, state: str, - attributes: Optional[Mapping] = None, + attributes: Optional[Mapping[str, Any]] = None, last_changed: Optional[datetime.datetime] = None, last_updated: Optional[datetime.datetime] = None, context: Optional[Context] = None, @@ -1093,7 +1103,7 @@ class StateMachine: self, entity_id: str, new_state: str, - attributes: Optional[Dict] = None, + attributes: Optional[Mapping[str, Any]] = None, force_update: bool = False, context: Optional[Context] = None, ) -> None: @@ -1142,7 +1152,7 @@ class StateMachine: self, entity_id: str, new_state: str, - attributes: Optional[Dict] = None, + attributes: Optional[Mapping[str, Any]] = None, force_update: bool = False, context: Optional[Context] = None, ) -> None: diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 9e204e91da5..77a1dc91dd7 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -13,6 +13,7 @@ FLOWS = [ "advantage_air", "agent_dvr", "airly", + "airnow", "airvisual", "alarmdecoder", "almond", @@ -53,6 +54,7 @@ FLOWS = [ "dynalite", "eafm", "ecobee", + "econet", "elgato", "elkm1", "emulated_roku", @@ -65,8 +67,10 @@ FLOWS = [ "flume", "flunearyou", "forked_daapd", + "foscam", "freebox", "fritzbox", + "fritzbox_callmonitor", "garmin_connect", "gdacs", "geofency", @@ -91,6 +95,7 @@ FLOWS = [ "homematicip_cloud", "huawei_lte", "hue", + "huisbaasje", "hunterdouglas_powerview", "hvv_departures", "hyperion", @@ -115,6 +120,7 @@ FLOWS = [ "locative", "logi_circle", "luftdaten", + "lutron_caseta", "mailgun", "melcloud", "met", @@ -139,6 +145,7 @@ FLOWS = [ "nws", "nzbget", "omnilogic", + "ondilo_ico", "onewire", "onvif", "opentherm_gw", @@ -188,6 +195,7 @@ FLOWS = [ "solarlog", "soma", "somfy", + "somfy_mylink", "sonarr", "songpal", "sonos", @@ -237,5 +245,6 @@ FLOWS = [ "yeelight", "zerproc", "zha", - "zwave" + "zwave", + "zwave_js" ] diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py new file mode 100644 index 00000000000..0b6f5166f88 --- /dev/null +++ b/homeassistant/generated/dhcp.py @@ -0,0 +1,138 @@ +"""Automatically generated by hassfest. + +To update, run python3 -m script.hassfest +""" + +# fmt: off + +DHCP = [ + { + "domain": "august", + "hostname": "connect", + "macaddress": "D86162*" + }, + { + "domain": "august", + "hostname": "connect", + "macaddress": "B8B7F1*" + }, + { + "domain": "axis", + "hostname": "axis-00408c*", + "macaddress": "00408C*" + }, + { + "domain": "axis", + "hostname": "axis-accc8e*", + "macaddress": "ACCC8E*" + }, + { + "domain": "axis", + "hostname": "axis-b8a44f*", + "macaddress": "B8A44F*" + }, + { + "domain": "flume", + "hostname": "flume-gw-*", + "macaddress": "ECFABC*" + }, + { + "domain": "flume", + "hostname": "flume-gw-*", + "macaddress": "B4E62D*" + }, + { + "domain": "nest", + "macaddress": "18B430*" + }, + { + "domain": "nexia", + "hostname": "xl857-*", + "macaddress": "000231*" + }, + { + "domain": "nuheat", + "hostname": "nuheat", + "macaddress": "002338*" + }, + { + "domain": "powerwall", + "hostname": "1118431-*", + "macaddress": "88DA1A*" + }, + { + "domain": "powerwall", + "hostname": "1118431-*", + "macaddress": "000145*" + }, + { + "domain": "rachio", + "hostname": "rachio-*", + "macaddress": "009D6B*" + }, + { + "domain": "rachio", + "hostname": "rachio-*", + "macaddress": "F0038C*" + }, + { + "domain": "rachio", + "hostname": "rachio-*", + "macaddress": "74C63B*" + }, + { + "domain": "ring", + "hostname": "ring*", + "macaddress": "0CAE7D*" + }, + { + "domain": "roomba", + "hostname": "irobot-*", + "macaddress": "501479*" + }, + { + "domain": "sense", + "hostname": "sense-*", + "macaddress": "009D6B*" + }, + { + "domain": "sense", + "hostname": "sense-*", + "macaddress": "DCEFCA*" + }, + { + "domain": "solaredge", + "hostname": "target", + "macaddress": "002702*" + }, + { + "domain": "somfy_mylink", + "hostname": "somfy_*", + "macaddress": "B8B7F1*" + }, + { + "domain": "squeezebox", + "hostname": "squeezebox*", + "macaddress": "000420*" + }, + { + "domain": "tesla", + "hostname": "tesla_*", + "macaddress": "4CFCAA*" + }, + { + "domain": "tesla", + "hostname": "tesla_*", + "macaddress": "044EAF*" + }, + { + "domain": "tesla", + "hostname": "tesla_*", + "macaddress": "98ED5C*" + }, + { + "domain": "toon", + "hostname": "eneco-*", + "macaddress": "74C63B*" + } +] diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 1617cd35435..8d28a499aaf 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -12,6 +12,11 @@ SSDP = { "manufacturer": "ARCAM" } ], + "axis": [ + { + "manufacturer": "AXIS" + } + ], "control4": [ { "st": "c4:director" @@ -166,6 +171,18 @@ SSDP = { "manufacturer": "Synology" } ], + "unifi": [ + { + "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", + "manufacturer": "Ubiquiti Networks", + "modelDescription": "UniFi Dream Machine" + }, + { + "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", + "manufacturer": "Ubiquiti Networks", + "modelDescription": "UniFi Dream Machine Pro" + } + ], "upnp": [ { "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1" diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 49527666f53..5521ab9da8f 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -90,6 +90,17 @@ ZEROCONF = { "domain": "ipp" } ], + "_kizbox._tcp.local.": [ + { + "domain": "somfy", + "name": "gateway*" + } + ], + "_leap._tcp.local.": [ + { + "domain": "lutron_caseta" + } + ], "_mediaremotetv._tcp.local.": [ { "domain": "apple_tv" @@ -165,6 +176,7 @@ HOMEKIT = { "Abode": "abode", "BSB002": "hue", "C105X": "roku", + "C135X": "roku", "Healty Home Coach": "netatmo", "Iota": "abode", "LIFX": "lifx", @@ -172,10 +184,12 @@ HOMEKIT = { "PowerView": "hunterdouglas_powerview", "Presence": "netatmo", "Rachio": "rachio", + "Smart Bridge": "lutron_caseta", "Socket": "wemo", "TRADFRI": "tradfri", "Welcome": "netatmo", "Wemo": "wemo", "iSmartGate": "gogogate2", + "iZone": "izone", "tado": "tado" } diff --git a/homeassistant/helpers/aiohttp_client.py b/homeassistant/helpers/aiohttp_client.py index fe995222c67..3e1e45e5981 100644 --- a/homeassistant/helpers/aiohttp_client.py +++ b/homeassistant/helpers/aiohttp_client.py @@ -129,7 +129,7 @@ async def async_aiohttp_proxy_stream( await response.prepare(request) try: - while True: + while hass.is_running: with async_timeout.timeout(timeout): data = await stream.read(buffer_size) diff --git a/homeassistant/helpers/check_config.py b/homeassistant/helpers/check_config.py index c98b563ac7e..97445b8cee2 100644 --- a/homeassistant/helpers/check_config.py +++ b/homeassistant/helpers/check_config.py @@ -78,7 +78,7 @@ async def async_check_ha_config_file(hass: HomeAssistant) -> HomeAssistantConfig def _comp_error(ex: Exception, domain: str, config: ConfigType) -> None: """Handle errors from components: async_log_exception.""" - result.add_error(_format_config_error(ex, domain, config), domain, config) + result.add_error(_format_config_error(ex, domain, config)[0], domain, config) # Load configuration.yaml config_path = hass.config.path(YAML_CONFIG_FILE) diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 6b9df47c4d8..889981537c6 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -82,6 +82,7 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): async_step_ssdp = async_step_discovery async_step_mqtt = async_step_discovery async_step_homekit = async_step_discovery + async_step_dhcp = async_step_discovery async def async_step_import(self, _: Optional[Dict[str, Any]]) -> Dict[str, Any]: """Handle a flow initialized by import.""" diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index c4d7de3839e..653d07a333e 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -329,6 +329,7 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): async_step_ssdp = async_step_discovery async_step_zeroconf = async_step_discovery async_step_homekit = async_step_discovery + async_step_dhcp = async_step_discovery @classmethod def async_register_implementation( diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 0513c5c6e7e..acf6139708a 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -87,7 +87,7 @@ from homeassistant.helpers import ( template as template_helper, ) from homeassistant.helpers.logging import KeywordStyleAdapter -from homeassistant.util import sanitize_path, slugify as util_slugify +from homeassistant.util import raise_if_invalid_path, slugify as util_slugify import homeassistant.util.dt as dt_util # pylint: disable=invalid-name @@ -118,8 +118,10 @@ def path(value: Any) -> str: if not isinstance(value, str): raise vol.Invalid("Expected a string") - if sanitize_path(value) != value: - raise vol.Invalid("Invalid path") + try: + raise_if_invalid_path(value) + except ValueError as err: + raise vol.Invalid("Invalid path") from err return value @@ -556,7 +558,7 @@ def template(value: Optional[Any]) -> template_helper.Template: template_value = template_helper.Template(str(value)) # type: ignore try: - template_value.ensure_valid() # type: ignore[no-untyped-call] + template_value.ensure_valid() return template_value except TemplateError as ex: raise vol.Invalid(f"invalid template ({ex})") from ex @@ -574,7 +576,7 @@ def dynamic_template(value: Optional[Any]) -> template_helper.Template: template_value = template_helper.Template(str(value)) # type: ignore try: - template_value.ensure_valid() # type: ignore[no-untyped-call] + template_value.ensure_valid() return template_value except TemplateError as ex: raise vol.Invalid(f"invalid template ({ex})") from ex @@ -916,18 +918,18 @@ SERVICE_SCHEMA = vol.All( has_at_least_one_key(CONF_SERVICE, CONF_SERVICE_TEMPLATE), ) +NUMERIC_STATE_THRESHOLD_SCHEMA = vol.Any( + vol.Coerce(float), vol.All(str, entity_domain("input_number")) +) + NUMERIC_STATE_CONDITION_SCHEMA = vol.All( vol.Schema( { vol.Required(CONF_CONDITION): "numeric_state", vol.Required(CONF_ENTITY_ID): entity_ids, vol.Optional(CONF_ATTRIBUTE): str, - CONF_BELOW: vol.Any( - vol.Coerce(float), vol.All(str, entity_domain("input_number")) - ), - CONF_ABOVE: vol.Any( - vol.Coerce(float), vol.All(str, entity_domain("input_number")) - ), + CONF_BELOW: NUMERIC_STATE_THRESHOLD_SCHEMA, + CONF_ABOVE: NUMERIC_STATE_THRESHOLD_SCHEMA, vol.Optional(CONF_VALUE_TEMPLATE): template, } ), diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index a62a2e63804..7478a7fede9 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -1,8 +1,11 @@ """Deprecation helpers for Home Assistant.""" +import functools import inspect import logging from typing import Any, Callable, Dict, Optional +from ..helpers.frame import MissingIntegrationFrame, get_integration_frame + def deprecated_substitute(substitute_name: str) -> Callable[..., Callable]: """Help migrate properties to new names. @@ -73,3 +76,43 @@ def get_deprecated( ) return config.get(old_name) return config.get(new_name, default) + + +def deprecated_function(replacement: str) -> Callable[..., Callable]: + """Mark function as deprecated and provide a replacement function to be used instead.""" + + def deprecated_decorator(func: Callable) -> Callable: + """Decorate function as deprecated.""" + + @functools.wraps(func) + def deprecated_func(*args: tuple, **kwargs: Dict[str, Any]) -> Any: + """Wrap for the original function.""" + logger = logging.getLogger(func.__module__) + try: + _, integration, path = get_integration_frame() + if path == "custom_components/": + logger.warning( + "%s was called from %s, this is a deprecated function. Use %s instead, please report this to the maintainer of %s", + func.__name__, + integration, + replacement, + integration, + ) + else: + logger.warning( + "%s was called from %s, this is a deprecated function. Use %s instead", + func.__name__, + integration, + replacement, + ) + except MissingIntegrationFrame: + logger.warning( + "%s is a deprecated function. Use %s instead", + func.__name__, + replacement, + ) + return func(*args, **kwargs) + + return deprecated_func + + return deprecated_decorator diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 6e8c09bbd60..c449d2ed4d0 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -1,6 +1,7 @@ """Provide a way to connect entities belonging to one device.""" from collections import OrderedDict import logging +import time from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple, Union import attr @@ -11,13 +12,13 @@ import homeassistant.util.uuid as uuid_util from .debounce import Debouncer from .singleton import singleton -from .typing import UNDEFINED, HomeAssistantType +from .typing import UNDEFINED, HomeAssistantType, UndefinedType + +# mypy: disallow_any_generics if TYPE_CHECKING: from . import entity_registry -# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs - _LOGGER = logging.getLogger(__name__) DATA_REGISTRY = "device_registry" @@ -39,25 +40,7 @@ DELETED_DEVICE = "deleted" DISABLED_INTEGRATION = "integration" DISABLED_USER = "user" - -@attr.s(slots=True, frozen=True) -class DeletedDeviceEntry: - """Deleted Device Registry Entry.""" - - config_entries: Set[str] = attr.ib() - connections: Set[Tuple[str, str]] = attr.ib() - identifiers: Set[Tuple[str, str]] = attr.ib() - id: str = attr.ib() - - def to_device_entry(self, config_entry_id, connections, identifiers): - """Create DeviceEntry from DeletedDeviceEntry.""" - return DeviceEntry( - config_entries={config_entry_id}, - connections=self.connections & connections, - identifiers=self.identifiers & identifiers, - id=self.id, - is_new=True, - ) +ORPHANED_DEVICE_KEEP_SECONDS = 86400 * 30 @attr.s(slots=True, frozen=True) @@ -67,14 +50,14 @@ class DeviceEntry: config_entries: Set[str] = attr.ib(converter=set, factory=set) connections: Set[Tuple[str, str]] = attr.ib(converter=set, factory=set) identifiers: Set[Tuple[str, str]] = attr.ib(converter=set, factory=set) - manufacturer: str = attr.ib(default=None) - model: str = attr.ib(default=None) - name: str = attr.ib(default=None) - sw_version: str = attr.ib(default=None) - via_device_id: str = attr.ib(default=None) - area_id: str = attr.ib(default=None) - name_by_user: str = attr.ib(default=None) - entry_type: str = attr.ib(default=None) + manufacturer: Optional[str] = attr.ib(default=None) + model: Optional[str] = attr.ib(default=None) + name: Optional[str] = attr.ib(default=None) + sw_version: Optional[str] = attr.ib(default=None) + via_device_id: Optional[str] = attr.ib(default=None) + area_id: Optional[str] = attr.ib(default=None) + name_by_user: Optional[str] = attr.ib(default=None) + entry_type: Optional[str] = attr.ib(default=None) id: str = attr.ib(factory=uuid_util.random_uuid_hex) # This value is not stored, just used to keep track of events to fire. is_new: bool = attr.ib(default=False) @@ -95,6 +78,33 @@ class DeviceEntry: return self.disabled_by is not None +@attr.s(slots=True, frozen=True) +class DeletedDeviceEntry: + """Deleted Device Registry Entry.""" + + config_entries: Set[str] = attr.ib() + connections: Set[Tuple[str, str]] = attr.ib() + identifiers: Set[Tuple[str, str]] = attr.ib() + id: str = attr.ib() + orphaned_timestamp: Optional[float] = attr.ib() + + def to_device_entry( + self, + config_entry_id: str, + connections: Set[Tuple[str, str]], + identifiers: Set[Tuple[str, str]], + ) -> DeviceEntry: + """Create DeviceEntry from DeletedDeviceEntry.""" + return DeviceEntry( + # type ignores: likely https://github.com/python/mypy/issues/8625 + config_entries={config_entry_id}, # type: ignore[arg-type] + connections=self.connections & connections, # type: ignore[arg-type] + identifiers=self.identifiers & identifiers, # type: ignore[arg-type] + id=self.id, + is_new=True, + ) + + def format_mac(mac: str) -> str: """Format the mac address string for entry into dev reg.""" to_test = mac @@ -120,7 +130,7 @@ class DeviceRegistry: devices: Dict[str, DeviceEntry] deleted_devices: Dict[str, DeletedDeviceEntry] - _devices_index: Dict[str, Dict[str, Dict[str, str]]] + _devices_index: Dict[str, Dict[str, Dict[Tuple[str, str], str]]] def __init__(self, hass: HomeAssistantType) -> None: """Initialize the device registry.""" @@ -135,7 +145,9 @@ class DeviceRegistry: @callback def async_get_device( - self, identifiers: set, connections: set + self, + identifiers: Set[Tuple[str, str]], + connections: Optional[Set[Tuple[str, str]]] = None, ) -> Optional[DeviceEntry]: """Check if device is registered.""" device_id = self._async_get_device_id_from_index( @@ -146,7 +158,9 @@ class DeviceRegistry: return self.devices[device_id] def _async_get_deleted_device( - self, identifiers: set, connections: set + self, + identifiers: Set[Tuple[str, str]], + connections: Optional[Set[Tuple[str, str]]], ) -> Optional[DeletedDeviceEntry]: """Check if device is deleted.""" device_id = self._async_get_device_id_from_index( @@ -157,7 +171,10 @@ class DeviceRegistry: return self.deleted_devices[device_id] def _async_get_device_id_from_index( - self, index: str, identifiers: set, connections: set + self, + index: str, + identifiers: Set[Tuple[str, str]], + connections: Optional[Set[Tuple[str, str]]], ) -> Optional[str]: """Check if device has previously been registered.""" devices_index = self._devices_index[index] @@ -201,40 +218,40 @@ class DeviceRegistry: _remove_device_from_index(devices_index, old_device) _add_device_to_index(devices_index, new_device) - def _clear_index(self): + def _clear_index(self) -> None: """Clear the index.""" self._devices_index = { REGISTERED_DEVICE: {IDX_IDENTIFIERS: {}, IDX_CONNECTIONS: {}}, DELETED_DEVICE: {IDX_IDENTIFIERS: {}, IDX_CONNECTIONS: {}}, } - def _rebuild_index(self): + def _rebuild_index(self) -> None: """Create the index after loading devices.""" self._clear_index() for device in self.devices.values(): _add_device_to_index(self._devices_index[REGISTERED_DEVICE], device) - for device in self.deleted_devices.values(): - _add_device_to_index(self._devices_index[DELETED_DEVICE], device) + for deleted_device in self.deleted_devices.values(): + _add_device_to_index(self._devices_index[DELETED_DEVICE], deleted_device) @callback def async_get_or_create( self, *, - config_entry_id, - connections=None, - identifiers=None, - manufacturer=UNDEFINED, - model=UNDEFINED, - name=UNDEFINED, - default_manufacturer=UNDEFINED, - default_model=UNDEFINED, - default_name=UNDEFINED, - sw_version=UNDEFINED, - entry_type=UNDEFINED, - via_device=None, + config_entry_id: str, + connections: Optional[Set[Tuple[str, str]]] = None, + identifiers: Optional[Set[Tuple[str, str]]] = None, + manufacturer: Union[str, None, UndefinedType] = UNDEFINED, + model: Union[str, None, UndefinedType] = UNDEFINED, + name: Union[str, None, UndefinedType] = UNDEFINED, + default_manufacturer: Union[str, None, UndefinedType] = UNDEFINED, + default_model: Union[str, None, UndefinedType] = UNDEFINED, + default_name: Union[str, None, UndefinedType] = UNDEFINED, + sw_version: Union[str, None, UndefinedType] = UNDEFINED, + entry_type: Union[str, None, UndefinedType] = UNDEFINED, + via_device: Optional[Tuple[str, str]] = None, # To disable a device if it gets created - disabled_by=UNDEFINED, - ): + disabled_by: Union[str, None, UndefinedType] = UNDEFINED, + ) -> Optional[DeviceEntry]: """Get device. Create if it doesn't exist.""" if not identifiers and not connections: return None @@ -270,8 +287,8 @@ class DeviceRegistry: name = default_name if via_device is not None: - via = self.async_get_device({via_device}, set()) - via_device_id = via.id if via else UNDEFINED + via = self.async_get_device({via_device}) + via_device_id: Union[str, UndefinedType] = via.id if via else UNDEFINED else: via_device_id = UNDEFINED @@ -292,19 +309,19 @@ class DeviceRegistry: @callback def async_update_device( self, - device_id, + device_id: str, *, - area_id=UNDEFINED, - manufacturer=UNDEFINED, - model=UNDEFINED, - name=UNDEFINED, - name_by_user=UNDEFINED, - new_identifiers=UNDEFINED, - sw_version=UNDEFINED, - via_device_id=UNDEFINED, - remove_config_entry_id=UNDEFINED, - disabled_by=UNDEFINED, - ): + area_id: Union[str, None, UndefinedType] = UNDEFINED, + manufacturer: Union[str, None, UndefinedType] = UNDEFINED, + model: Union[str, None, UndefinedType] = UNDEFINED, + name: Union[str, None, UndefinedType] = UNDEFINED, + name_by_user: Union[str, None, UndefinedType] = UNDEFINED, + new_identifiers: Union[Set[Tuple[str, str]], UndefinedType] = UNDEFINED, + sw_version: Union[str, None, UndefinedType] = UNDEFINED, + via_device_id: Union[str, None, UndefinedType] = UNDEFINED, + remove_config_entry_id: Union[str, UndefinedType] = UNDEFINED, + disabled_by: Union[str, None, UndefinedType] = UNDEFINED, + ) -> Optional[DeviceEntry]: """Update properties of a device.""" return self._async_update_device( device_id, @@ -323,27 +340,27 @@ class DeviceRegistry: @callback def _async_update_device( self, - device_id, + device_id: str, *, - add_config_entry_id=UNDEFINED, - remove_config_entry_id=UNDEFINED, - merge_connections=UNDEFINED, - merge_identifiers=UNDEFINED, - new_identifiers=UNDEFINED, - manufacturer=UNDEFINED, - model=UNDEFINED, - name=UNDEFINED, - sw_version=UNDEFINED, - entry_type=UNDEFINED, - via_device_id=UNDEFINED, - area_id=UNDEFINED, - name_by_user=UNDEFINED, - disabled_by=UNDEFINED, - ): + add_config_entry_id: Union[str, UndefinedType] = UNDEFINED, + remove_config_entry_id: Union[str, UndefinedType] = UNDEFINED, + merge_connections: Union[Set[Tuple[str, str]], UndefinedType] = UNDEFINED, + merge_identifiers: Union[Set[Tuple[str, str]], UndefinedType] = UNDEFINED, + new_identifiers: Union[Set[Tuple[str, str]], UndefinedType] = UNDEFINED, + manufacturer: Union[str, None, UndefinedType] = UNDEFINED, + model: Union[str, None, UndefinedType] = UNDEFINED, + name: Union[str, None, UndefinedType] = UNDEFINED, + sw_version: Union[str, None, UndefinedType] = UNDEFINED, + entry_type: Union[str, None, UndefinedType] = UNDEFINED, + via_device_id: Union[str, None, UndefinedType] = UNDEFINED, + area_id: Union[str, None, UndefinedType] = UNDEFINED, + name_by_user: Union[str, None, UndefinedType] = UNDEFINED, + disabled_by: Union[str, None, UndefinedType] = UNDEFINED, + ) -> Optional[DeviceEntry]: """Update device attributes.""" old = self.devices[device_id] - changes = {} + changes: Dict[str, Any] = {} config_entries = old.config_entries @@ -359,21 +376,21 @@ class DeviceRegistry: ): if config_entries == {remove_config_entry_id}: self.async_remove_device(device_id) - return + return None config_entries = config_entries - {remove_config_entry_id} if config_entries != old.config_entries: changes["config_entries"] = config_entries - for attr_name, value in ( + for attr_name, setvalue in ( ("connections", merge_connections), ("identifiers", merge_identifiers), ): old_value = getattr(old, attr_name) # If not undefined, check if `value` contains new items. - if value is not UNDEFINED and not value.issubset(old_value): - changes[attr_name] = old_value | value + if setvalue is not UNDEFINED and not setvalue.issubset(old_value): + changes[attr_name] = old_value | setvalue if new_identifiers is not UNDEFINED: changes["identifiers"] = new_identifiers @@ -427,6 +444,7 @@ class DeviceRegistry: connections=device.connections, identifiers=device.identifiers, id=device.id, + orphaned_timestamp=None, ) ) self.hass.bus.async_fire( @@ -434,7 +452,7 @@ class DeviceRegistry: ) self.async_schedule_save() - async def async_load(self): + async def async_load(self) -> None: """Load the device registry.""" async_setup_cleanup(self.hass, self) @@ -447,8 +465,9 @@ class DeviceRegistry: for device in data["devices"]: devices[device["id"]] = DeviceEntry( config_entries=set(device["config_entries"]), - connections={tuple(conn) for conn in device["connections"]}, - identifiers={tuple(iden) for iden in device["identifiers"]}, + # type ignores (if tuple arg was cast): likely https://github.com/python/mypy/issues/8625 + connections={tuple(conn) for conn in device["connections"]}, # type: ignore[misc] + identifiers={tuple(iden) for iden in device["identifiers"]}, # type: ignore[misc] manufacturer=device["manufacturer"], model=device["model"], name=device["name"], @@ -471,9 +490,12 @@ class DeviceRegistry: for device in data.get("deleted_devices", []): deleted_devices[device["id"]] = DeletedDeviceEntry( config_entries=set(device["config_entries"]), - connections={tuple(conn) for conn in device["connections"]}, - identifiers={tuple(iden) for iden in device["identifiers"]}, + # type ignores (if tuple arg was cast): likely https://github.com/python/mypy/issues/8625 + connections={tuple(conn) for conn in device["connections"]}, # type: ignore[misc] + identifiers={tuple(iden) for iden in device["identifiers"]}, # type: ignore[misc] id=device["id"], + # Introduced in 2021.2 + orphaned_timestamp=device.get("orphaned_timestamp"), ) self.devices = devices @@ -514,6 +536,7 @@ class DeviceRegistry: "connections": list(entry.connections), "identifiers": list(entry.identifiers), "id": entry.id, + "orphaned_timestamp": entry.orphaned_timestamp, } for entry in self.deleted_devices.values() ] @@ -523,6 +546,7 @@ class DeviceRegistry: @callback def async_clear_config_entry(self, config_entry_id: str) -> None: """Clear config entry from registry entries.""" + now_time = time.time() for device in list(self.devices.values()): self._async_update_device(device.id, remove_config_entry_id=config_entry_id) for deleted_device in list(self.deleted_devices.values()): @@ -530,8 +554,10 @@ class DeviceRegistry: if config_entry_id not in config_entries: continue if config_entries == {config_entry_id}: - # Permanently remove the device from the device registry. - self._remove_device(deleted_device) + # Add a time stamp when the deleted device became orphaned + self.deleted_devices[deleted_device.id] = attr.evolve( + deleted_device, orphaned_timestamp=now_time, config_entries=set() + ) else: config_entries = config_entries - {config_entry_id} # No need to reindex here since we currently @@ -541,6 +567,24 @@ class DeviceRegistry: ) self.async_schedule_save() + @callback + def async_purge_expired_orphaned_devices(self) -> None: + """Purge expired orphaned devices from the registry. + + We need to purge these periodically to avoid the database + growing without bound. + """ + now_time = time.time() + for deleted_device in list(self.deleted_devices.values()): + if deleted_device.orphaned_timestamp is None: + continue + + if ( + deleted_device.orphaned_timestamp + ORPHANED_DEVICE_KEEP_SECONDS + < now_time + ): + self._remove_device(deleted_device) + @callback def async_clear_area_id(self, area_id: str) -> None: """Clear area id from registry entries.""" @@ -608,13 +652,17 @@ def async_cleanup( device.id, remove_config_entry_id=config_entry_id ) + # Periodic purge of orphaned devices to avoid the registry + # growing without bounds when there are lots of deleted devices + dev_reg.async_purge_expired_orphaned_devices() + @callback def async_setup_cleanup(hass: HomeAssistantType, dev_reg: DeviceRegistry) -> None: """Clean up device registry when entities removed.""" from . import entity_registry # pylint: disable=import-outside-toplevel - async def cleanup(): + async def cleanup() -> None: """Cleanup.""" ent_reg = await entity_registry.async_get_registry(hass) async_cleanup(hass, dev_reg, ent_reg) @@ -649,7 +697,7 @@ def async_setup_cleanup(hass: HomeAssistantType, dev_reg: DeviceRegistry) -> Non hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, startup_clean) -def _normalize_connections(connections: set) -> set: +def _normalize_connections(connections: Set[Tuple[str, str]]) -> Set[Tuple[str, str]]: """Normalize connections to ensure we can match mac addresses.""" return { (key, format_mac(value)) if key == CONNECTION_NETWORK_MAC else (key, value) @@ -658,7 +706,8 @@ def _normalize_connections(connections: set) -> set: def _add_device_to_index( - devices_index: dict, device: Union[DeviceEntry, DeletedDeviceEntry] + devices_index: Dict[str, Dict[Tuple[str, str], str]], + device: Union[DeviceEntry, DeletedDeviceEntry], ) -> None: """Add a device to the index.""" for identifier in device.identifiers: @@ -668,7 +717,8 @@ def _add_device_to_index( def _remove_device_from_index( - devices_index: dict, device: Union[DeviceEntry, DeletedDeviceEntry] + devices_index: Dict[str, Dict[Tuple[str, str], str]], + device: Union[DeviceEntry, DeletedDeviceEntry], ) -> None: """Remove a device from the index.""" for identifier in device.identifiers: diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 7b38c102253..bd687ab7ce8 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -26,7 +26,6 @@ from .event import async_call_later, async_track_time_interval if TYPE_CHECKING: from .entity import Entity -# mypy: allow-untyped-defs SLOW_SETUP_WARNING = 10 SLOW_SETUP_MAX_WAIT = 60 @@ -81,7 +80,7 @@ class EntityPlatform: self.platform_name, [] ).append(self) - def __repr__(self): + def __repr__(self) -> str: """Represent an EntityPlatform.""" return f"" @@ -116,7 +115,7 @@ class EntityPlatform: return self.parallel_updates - async def async_setup(self, platform_config, discovery_info=None): + async def async_setup(self, platform_config, discovery_info=None): # type: ignore[no-untyped-def] """Set up the platform from a config file.""" platform = self.platform hass = self.hass @@ -162,7 +161,7 @@ class EntityPlatform: platform = self.platform @callback - def async_create_setup_task(): + def async_create_setup_task(): # type: ignore[no-untyped-def] """Get task to set up platform.""" return platform.async_setup_entry( # type: ignore self.hass, config_entry, self._async_schedule_add_entities @@ -218,7 +217,7 @@ class EntityPlatform: wait_time, ) - async def setup_again(now): + async def setup_again(now): # type: ignore[no-untyped-def] """Run setup again.""" self._async_cancel_retry_setup = None await self._async_setup_platform(async_create_setup_task, tries) @@ -340,7 +339,7 @@ class EntityPlatform: self.scan_interval, ) - async def _async_add_entity( + async def _async_add_entity( # type: ignore[no-untyped-def] self, entity, update_before_add, entity_registry, device_registry ): """Add an entity to the platform.""" @@ -560,7 +559,7 @@ class EntityPlatform: ) @callback - def async_register_entity_service(self, name, schema, func, required_features=None): + def async_register_entity_service(self, name, schema, func, required_features=None): # type: ignore[no-untyped-def] """Register an entity service. Services will automatically be shared by all platforms of the same domain. diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 44f5c9c56f7..0628c1e0eb5 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -18,7 +18,7 @@ from typing import ( List, Optional, Tuple, - cast, + Union, ) import attr @@ -39,13 +39,11 @@ from homeassistant.util import slugify from homeassistant.util.yaml import load_yaml from .singleton import singleton -from .typing import UNDEFINED, HomeAssistantType +from .typing import UNDEFINED, HomeAssistantType, UndefinedType if TYPE_CHECKING: from homeassistant.config_entries import ConfigEntry # noqa: F401 -# mypy: allow-untyped-defs, no-check-untyped-defs - PATH_REGISTRY = "entity_registry.yaml" DATA_REGISTRY = "entity_registry" EVENT_ENTITY_REGISTRY_UPDATED = "entity_registry_updated" @@ -222,7 +220,7 @@ class EntityRegistry: entity_id = self.async_get_entity_id(domain, platform, unique_id) if entity_id: - return self._async_update_entity( # type: ignore + return self._async_update_entity( entity_id, config_entry_id=config_entry_id or UNDEFINED, device_id=device_id or UNDEFINED, @@ -316,63 +314,56 @@ class EntityRegistry: for entity in entities: if entity.disabled_by != DISABLED_DEVICE: continue - self.async_update_entity( # type: ignore - entity.entity_id, disabled_by=None - ) + self.async_update_entity(entity.entity_id, disabled_by=None) return entities = async_entries_for_device(self, event.data["device_id"]) for entity in entities: - self.async_update_entity( # type: ignore - entity.entity_id, disabled_by=DISABLED_DEVICE - ) + self.async_update_entity(entity.entity_id, disabled_by=DISABLED_DEVICE) @callback def async_update_entity( self, - entity_id, + entity_id: str, *, - name=UNDEFINED, - icon=UNDEFINED, - area_id=UNDEFINED, - new_entity_id=UNDEFINED, - new_unique_id=UNDEFINED, - disabled_by=UNDEFINED, - ): + name: Union[str, None, UndefinedType] = UNDEFINED, + icon: Union[str, None, UndefinedType] = UNDEFINED, + area_id: Union[str, None, UndefinedType] = UNDEFINED, + new_entity_id: Union[str, UndefinedType] = UNDEFINED, + new_unique_id: Union[str, UndefinedType] = UNDEFINED, + disabled_by: Union[str, None, UndefinedType] = UNDEFINED, + ) -> RegistryEntry: """Update properties of an entity.""" - return cast( # cast until we have _async_update_entity type hinted - RegistryEntry, - self._async_update_entity( - entity_id, - name=name, - icon=icon, - area_id=area_id, - new_entity_id=new_entity_id, - new_unique_id=new_unique_id, - disabled_by=disabled_by, - ), + return self._async_update_entity( + entity_id, + name=name, + icon=icon, + area_id=area_id, + new_entity_id=new_entity_id, + new_unique_id=new_unique_id, + disabled_by=disabled_by, ) @callback def _async_update_entity( self, - entity_id, + entity_id: str, *, - name=UNDEFINED, - icon=UNDEFINED, - config_entry_id=UNDEFINED, - new_entity_id=UNDEFINED, - device_id=UNDEFINED, - area_id=UNDEFINED, - new_unique_id=UNDEFINED, - disabled_by=UNDEFINED, - capabilities=UNDEFINED, - supported_features=UNDEFINED, - device_class=UNDEFINED, - unit_of_measurement=UNDEFINED, - original_name=UNDEFINED, - original_icon=UNDEFINED, - ): + name: Union[str, None, UndefinedType] = UNDEFINED, + icon: Union[str, None, UndefinedType] = UNDEFINED, + config_entry_id: Union[str, None, UndefinedType] = UNDEFINED, + new_entity_id: Union[str, UndefinedType] = UNDEFINED, + device_id: Union[str, None, UndefinedType] = UNDEFINED, + area_id: Union[str, None, UndefinedType] = UNDEFINED, + new_unique_id: Union[str, UndefinedType] = UNDEFINED, + disabled_by: Union[str, None, UndefinedType] = UNDEFINED, + capabilities: Union[Dict[str, Any], None, UndefinedType] = UNDEFINED, + supported_features: Union[int, UndefinedType] = UNDEFINED, + device_class: Union[str, None, UndefinedType] = UNDEFINED, + unit_of_measurement: Union[str, None, UndefinedType] = UNDEFINED, + original_name: Union[str, None, UndefinedType] = UNDEFINED, + original_icon: Union[str, None, UndefinedType] = UNDEFINED, + ) -> RegistryEntry: """Private facing update properties method.""" old = self.entities[entity_id] @@ -397,7 +388,7 @@ class EntityRegistry: if new_entity_id is not UNDEFINED and new_entity_id != old.entity_id: if self.async_is_registered(new_entity_id): - raise ValueError("Entity is already registered") + raise ValueError("Entity with this ID is already registered") if not valid_entity_id(new_entity_id): raise ValueError("Invalid entity ID") @@ -526,7 +517,7 @@ class EntityRegistry: """Clear area id from registry entries.""" for entity_id, entry in self.entities.items(): if area_id == entry.area_id: - self._async_update_entity(entity_id, area_id=None) # type: ignore + self._async_update_entity(entity_id, area_id=None) def _register_entry(self, entry: RegistryEntry) -> None: self.entities[entry.entity_id] = entry diff --git a/homeassistant/helpers/entity_values.py b/homeassistant/helpers/entity_values.py index de48219a8d1..7f44e8b2768 100644 --- a/homeassistant/helpers/entity_values.py +++ b/homeassistant/helpers/entity_values.py @@ -6,18 +6,20 @@ from typing import Any, Dict, Optional, Pattern from homeassistant.core import split_entity_id +# mypy: disallow-any-generics + class EntityValues: """Class to store entity id based values.""" def __init__( self, - exact: Optional[Dict] = None, - domain: Optional[Dict] = None, - glob: Optional[Dict] = None, + exact: Optional[Dict[str, Dict[str, str]]] = None, + domain: Optional[Dict[str, Dict[str, str]]] = None, + glob: Optional[Dict[str, Dict[str, str]]] = None, ) -> None: """Initialize an EntityConfigDict.""" - self._cache: Dict[str, Dict] = {} + self._cache: Dict[str, Dict[str, str]] = {} self._exact = exact self._domain = domain @@ -30,7 +32,7 @@ class EntityValues: self._glob = compiled - def get(self, entity_id: str) -> Dict: + def get(self, entity_id: str) -> Dict[str, str]: """Get config for an entity id.""" if entity_id in self._cache: return self._cache[entity_id] diff --git a/homeassistant/helpers/intent.py b/homeassistant/helpers/intent.py index df5e40911ff..f8c8b2c6d8c 100644 --- a/homeassistant/helpers/intent.py +++ b/homeassistant/helpers/intent.py @@ -169,10 +169,13 @@ def _fuzzymatch(name: str, items: Iterable[T], key: Callable[[T], str]) -> Optio for idx, item in enumerate(items): match = regex.search(key(item)) if match: - # Add index so we pick first match in case same group and start - matches.append((len(match.group()), match.start(), idx, item)) + # Add key length so we prefer shorter keys with the same group and start. + # Add index so we pick first match in case same group, start, and key length. + matches.append( + (len(match.group()), match.start(), len(key(item)), idx, item) + ) - return sorted(matches)[0][3] if matches else None + return sorted(matches)[0][4] if matches else None class ServiceIntentHandler(IntentHandler): diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 48a662e3a81..f197664f7e6 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -219,7 +219,7 @@ class _ScriptRun: self._stop = asyncio.Event() self._stopped = asyncio.Event() - def _changed(self): + def _changed(self) -> None: if not self._stop.is_set(): self._script._changed() # pylint: disable=protected-access @@ -227,8 +227,12 @@ class _ScriptRun: # pylint: disable=protected-access return await self._script._async_get_condition(config) - def _log(self, msg, *args, level=logging.INFO): - self._script._log(msg, *args, level=level) # pylint: disable=protected-access + def _log( + self, msg: str, *args: Any, level: int = logging.INFO, **kwargs: Any + ) -> None: + self._script._log( # pylint: disable=protected-access + msg, *args, level=level, **kwargs + ) async def async_run(self) -> None: """Run script.""" @@ -257,7 +261,7 @@ class _ScriptRun: self._log_exception(ex) raise - def _finish(self): + def _finish(self) -> None: self._script._runs.remove(self) # pylint: disable=protected-access if not self._script.is_running: self._script.last_action = None @@ -287,6 +291,9 @@ class _ScriptRun: elif isinstance(exception, exceptions.ServiceNotFound): error_desc = "Service not found" + elif isinstance(exception, exceptions.HomeAssistantError): + error_desc = "Error" + else: error_desc = "Unexpected error" level = _LOG_EXCEPTION @@ -389,7 +396,7 @@ class _ScriptRun: async def _async_run_long_action(self, long_task): """Run a long task while monitoring for stop request.""" - async def async_cancel_long_task(): + async def async_cancel_long_task() -> None: # Stop long task and wait for it to finish. long_task.cancel() try: @@ -586,7 +593,7 @@ class _ScriptRun: else: del self._variables["repeat"] - async def _async_choose_step(self): + async def _async_choose_step(self) -> None: """Choose a sequence.""" # pylint: disable=protected-access choose_data = await self._script._async_get_choose_data(self._step) @@ -623,8 +630,8 @@ class _ScriptRun: } done.set() - def log_cb(level, msg): - self._log(msg, level=level) + def log_cb(level, msg, **kwargs): + self._log(msg, level=level, **kwargs) to_context = None remove_triggers = await async_initialize_triggers( @@ -706,7 +713,7 @@ class _QueuedScriptRun(_ScriptRun): else: await super().async_run() - def _finish(self): + def _finish(self) -> None: # pylint: disable=protected-access if self.lock_acquired: self._script._queue_lck.release() @@ -868,7 +875,7 @@ class Script: if choose_data["default"]: choose_data["default"].update_logger(self._logger) - def _changed(self): + def _changed(self) -> None: if self._change_listener_job: self._hass.async_run_hass_job(self._change_listener_job) @@ -898,7 +905,7 @@ class Script: if self._referenced_devices is not None: return self._referenced_devices - referenced = set() + referenced: Set[str] = set() for step in self.sequence: action = cv.determine_script_action(step) @@ -927,7 +934,7 @@ class Script: if self._referenced_entities is not None: return self._referenced_entities - referenced = set() + referenced: Set[str] = set() for step in self.sequence: action = cv.determine_script_action(step) @@ -1128,11 +1135,13 @@ class Script: self._choose_data[step] = choose_data return choose_data - def _log(self, msg, *args, level=logging.INFO): + def _log( + self, msg: str, *args: Any, level: int = logging.INFO, **kwargs: Any + ) -> None: msg = f"%s: {msg}" - args = [self.name, *args] + args = (self.name, *args) if level == _LOG_EXCEPTION: - self._logger.exception(msg, *args) + self._logger.exception(msg, *args, **kwargs) else: - self._logger.log(level, msg, *args) + self._logger.log(level, msg, *args, **kwargs) diff --git a/homeassistant/helpers/selector.py b/homeassistant/helpers/selector.py index 5cc7ada1bc5..b48ffb6e964 100644 --- a/homeassistant/helpers/selector.py +++ b/homeassistant/helpers/selector.py @@ -162,3 +162,17 @@ class ActionSelector(Selector): """Selector of an action sequence (script syntax).""" CONFIG_SCHEMA = vol.Schema({}) + + +@SELECTORS.register("object") +class ObjectSelector(Selector): + """Selector for an arbitrary object.""" + + CONFIG_SCHEMA = vol.Schema({}) + + +@SELECTORS.register("text") +class StringSelector(Selector): + """Selector for a multi-line text string.""" + + CONFIG_SCHEMA = vol.Schema({vol.Optional("multiline", default=False): bool}) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 25a88bb59cb..c95f942c6dc 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -128,13 +128,15 @@ async def async_call_from_config( ) -> None: """Call a service based on a config hash.""" try: - parms = async_prepare_call_from_config(hass, config, variables, validate_config) + params = async_prepare_call_from_config( + hass, config, variables, validate_config + ) except HomeAssistantError as ex: if blocking: raise _LOGGER.error(ex) else: - await hass.services.async_call(*parms, blocking, context) + await hass.services.async_call(*params, blocking, context) @ha.callback diff --git a/homeassistant/helpers/significant_change.py b/homeassistant/helpers/significant_change.py new file mode 100644 index 00000000000..0a5b6aae10d --- /dev/null +++ b/homeassistant/helpers/significant_change.py @@ -0,0 +1,166 @@ +"""Helpers to help find if an entity has changed significantly. + +Does this with help of the integration. Looks at significant_change.py +platform for a function `async_check_significant_change`: + +```python +from typing import Optional +from homeassistant.core import HomeAssistant + +async def async_check_significant_change( + hass: HomeAssistant, + old_state: str, + old_attrs: dict, + new_state: str, + new_attrs: dict, + **kwargs, +) -> Optional[bool] +``` + +Return boolean to indicate if significantly changed. If don't know, return None. + +**kwargs will allow us to expand this feature in the future, like passing in a +level of significance. + +The following cases will never be passed to your function: +- if either state is unknown/unavailable +- state adding/removing +""" +from types import MappingProxyType +from typing import Any, Callable, Dict, Optional, Union + +from homeassistant.const import STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.core import HomeAssistant, State, callback + +from .integration_platform import async_process_integration_platforms + +PLATFORM = "significant_change" +DATA_FUNCTIONS = "significant_change" +CheckTypeFunc = Callable[ + [ + HomeAssistant, + str, + Union[dict, MappingProxyType], + str, + Union[dict, MappingProxyType], + ], + Optional[bool], +] + + +async def create_checker( + hass: HomeAssistant, _domain: str +) -> "SignificantlyChangedChecker": + """Create a significantly changed checker for a domain.""" + await _initialize(hass) + return SignificantlyChangedChecker(hass) + + +# Marked as singleton so multiple calls all wait for same output. +async def _initialize(hass: HomeAssistant) -> None: + """Initialize the functions.""" + if DATA_FUNCTIONS in hass.data: + return + + functions = hass.data[DATA_FUNCTIONS] = {} + + async def process_platform( + hass: HomeAssistant, component_name: str, platform: Any + ) -> None: + """Process a significant change platform.""" + functions[component_name] = platform.async_check_significant_change + + await async_process_integration_platforms(hass, PLATFORM, process_platform) + + +def either_one_none(val1: Optional[Any], val2: Optional[Any]) -> bool: + """Test if exactly one value is None.""" + return (val1 is None and val2 is not None) or (val1 is not None and val2 is None) + + +def check_numeric_changed( + val1: Optional[Union[int, float]], + val2: Optional[Union[int, float]], + change: Union[int, float], +) -> bool: + """Check if two numeric values have changed.""" + if val1 is None and val2 is None: + return False + + if either_one_none(val1, val2): + return True + + assert val1 is not None + assert val2 is not None + + if abs(val1 - val2) >= change: + return True + + return False + + +class SignificantlyChangedChecker: + """Class to keep track of entities to see if they have significantly changed. + + Will always compare the entity to the last entity that was considered significant. + """ + + def __init__(self, hass: HomeAssistant) -> None: + """Test if an entity has significantly changed.""" + self.hass = hass + self.last_approved_entities: Dict[str, State] = {} + + @callback + def async_is_significant_change(self, new_state: State) -> bool: + """Return if this was a significant change.""" + old_state: Optional[State] = self.last_approved_entities.get( + new_state.entity_id + ) + + # First state change is always ok to report + if old_state is None: + self.last_approved_entities[new_state.entity_id] = new_state + return True + + # Handle state unknown or unavailable + if new_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE): + if new_state.state == old_state.state: + return False + + self.last_approved_entities[new_state.entity_id] = new_state + return True + + # If last state was unknown/unavailable, also significant. + if old_state.state in (STATE_UNKNOWN, STATE_UNAVAILABLE): + self.last_approved_entities[new_state.entity_id] = new_state + return True + + functions: Optional[Dict[str, CheckTypeFunc]] = self.hass.data.get( + DATA_FUNCTIONS + ) + + if functions is None: + raise RuntimeError("Significant Change not initialized") + + check_significantly_changed = functions.get(new_state.domain) + + # No platform available means always true. + if check_significantly_changed is None: + self.last_approved_entities[new_state.entity_id] = new_state + return True + + result = check_significantly_changed( + self.hass, + old_state.state, + old_state.attributes, + new_state.state, + new_state.attributes, + ) + + if result is False: + return False + + # Result is either True or None. + # None means the function doesn't know. For now assume it's True + self.last_approved_entities[new_state.entity_id] = new_state + return True diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index fb3e6ba40b5..5f506c02eef 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -11,7 +11,7 @@ import math from operator import attrgetter import random import re -from typing import Any, Dict, Generator, Iterable, Optional, Type, Union +from typing import Any, Dict, Generator, Iterable, Optional, Type, Union, cast from urllib.parse import urlencode as urllib_urlencode import weakref @@ -38,8 +38,7 @@ from homeassistant.util import convert, dt as dt_util, location as loc_util from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.thread import ThreadWithException -# mypy: allow-untyped-calls, allow-untyped-defs -# mypy: no-check-untyped-defs, no-warn-return-any +# mypy: allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) _SENTINEL = object() @@ -140,7 +139,7 @@ def gen_result_wrapper(kls): if kls is set: return str(set(self)) - return kls.__str__(self) + return cast(str, kls.__str__(self)) return self.render_result @@ -173,7 +172,8 @@ class TupleWrapper(tuple, ResultWrapper): RESULT_WRAPPERS: Dict[Type, Type] = { - kls: gen_result_wrapper(kls) for kls in (list, dict, set) + kls: gen_result_wrapper(kls) # type: ignore[no-untyped-call] + for kls in (list, dict, set) } RESULT_WRAPPERS[tuple] = TupleWrapper @@ -195,15 +195,15 @@ class RenderInfo: # Will be set sensibly once frozen. self.filter_lifecycle = _true self.filter = _true - self._result = None + self._result: Optional[str] = None self.is_static = False - self.exception = None + self.exception: Optional[TemplateError] = None self.all_states = False self.all_states_lifecycle = False self.domains = set() self.domains_lifecycle = set() self.entities = set() - self.rate_limit = None + self.rate_limit: Optional[timedelta] = None self.has_time = False def __repr__(self) -> str: @@ -228,7 +228,7 @@ class RenderInfo: """Results of the template computation.""" if self.exception is not None: raise self.exception - return self._result + return cast(str, self._result) def _freeze_static(self) -> None: self.is_static = True @@ -288,26 +288,26 @@ class Template: self.template: str = template.strip() self._compiled_code = None - self._compiled = None + self._compiled: Optional[Template] = None self.hass = hass self.is_static = not is_template_string(template) @property - def _env(self): + def _env(self) -> "TemplateEnvironment": if self.hass is None: return _NO_HASS_ENV - ret = self.hass.data.get(_ENVIRONMENT) + ret: Optional[TemplateEnvironment] = self.hass.data.get(_ENVIRONMENT) if ret is None: - ret = self.hass.data[_ENVIRONMENT] = TemplateEnvironment(self.hass) + ret = self.hass.data[_ENVIRONMENT] = TemplateEnvironment(self.hass) # type: ignore[no-untyped-call] return ret - def ensure_valid(self): + def ensure_valid(self) -> None: """Return if template is valid.""" if self._compiled_code is not None: return try: - self._compiled_code = self._env.compile(self.template) + self._compiled_code = self._env.compile(self.template) # type: ignore[no-untyped-call] except jinja2.TemplateError as err: raise TemplateError(err) from err @@ -422,7 +422,7 @@ class Template: finish_event = asyncio.Event() - def _render_template(): + def _render_template() -> None: try: compiled.render(kwargs) except TimeoutError: @@ -449,7 +449,7 @@ class Template: """Render the template and collect an entity filter.""" assert self.hass and _RENDER_INFO not in self.hass.data - render_info = RenderInfo(self) + render_info = RenderInfo(self) # type: ignore[no-untyped-call] # pylint: disable=protected-access if self.is_static: @@ -519,7 +519,7 @@ class Template: ) return value if error_value is _SENTINEL else error_value - def _ensure_compiled(self): + def _ensure_compiled(self) -> "Template": """Bind a template to a specific hass instance.""" self.ensure_valid() @@ -527,8 +527,9 @@ class Template: env = self._env - self._compiled = jinja2.Template.from_code( - env, self._compiled_code, env.globals, None + self._compiled = cast( + Template, + jinja2.Template.from_code(env, self._compiled_code, env.globals, None), ) return self._compiled @@ -553,7 +554,7 @@ class Template: class AllStates: """Class to expose all HA states as attributes.""" - def __init__(self, hass): + def __init__(self, hass: HomeAssistantType) -> None: """Initialize all states.""" self._hass = hass @@ -607,7 +608,7 @@ class AllStates: class DomainStates: """Class to expose a specific HA domain as attributes.""" - def __init__(self, hass, domain): + def __init__(self, hass: HomeAssistantType, domain: str) -> None: """Initialize the domain states.""" self._hass = hass self._domain = domain @@ -652,13 +653,15 @@ class TemplateState(State): # Inheritance is done so functions that check against State keep working # pylint: disable=super-init-not-called - def __init__(self, hass, state, collect=True): + def __init__( + self, hass: HomeAssistantType, state: State, collect: bool = True + ) -> None: """Initialize template state.""" self._hass = hass self._state = state self._collect = collect - def _collect_state(self): + def _collect_state(self) -> None: if self._collect and _RENDER_INFO in self._hass.data: self._hass.data[_RENDER_INFO].entities.add(self._state.entity_id) @@ -1411,4 +1414,4 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): return cached -_NO_HASS_ENV = TemplateEnvironment(None) +_NO_HASS_ENV = TemplateEnvironment(None) # type: ignore[no-untyped-call] diff --git a/homeassistant/helpers/trigger.py b/homeassistant/helpers/trigger.py index f9dd91dc2f1..2c7275a9cc3 100644 --- a/homeassistant/helpers/trigger.py +++ b/homeassistant/helpers/trigger.py @@ -75,12 +75,19 @@ async def async_initialize_triggers( platform = await _async_get_trigger_platform(hass, conf) triggers.append(platform.async_attach_trigger(hass, conf, action, info)) - removes = await asyncio.gather(*triggers) + attach_results = await asyncio.gather(*triggers, return_exceptions=True) + removes = [] - if None in removes: - log_cb(logging.ERROR, "Error setting up trigger") + for result in attach_results: + if isinstance(result, Exception): + log_cb(logging.ERROR, "Error setting up trigger", exc_info=result) + elif result is None: + log_cb( + logging.ERROR, "Unknown error while setting up trigger (empty result)" + ) + else: + removes.append(result) - removes = list(filter(None, removes)) if not removes: return None diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 895eff01f57..8df2c57b1e7 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -9,7 +9,8 @@ import urllib.error import aiohttp import requests -from homeassistant.core import CALLBACK_TYPE, HassJob, HomeAssistant, callback +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback from homeassistant.helpers import entity, event from homeassistant.util.dt import utcnow @@ -66,6 +67,10 @@ class DataUpdateCoordinator(Generic[T]): self._debounced_refresh = request_refresh_debouncer + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_STOP, self._async_stop_refresh + ) + @callback def async_add_listener(self, update_callback: CALLBACK_TYPE) -> Callable[[], None]: """Listen for data updates.""" @@ -214,6 +219,14 @@ class DataUpdateCoordinator(Generic[T]): for update_callback in self._listeners: update_callback() + @callback + def _async_stop_refresh(self, _: Event) -> None: + """Stop refreshing when Home Assistant is stopping.""" + self.update_interval = None + if self._unsub_refresh: + self._unsub_refresh() + self._unsub_refresh = None + class CoordinatorEntity(entity.Entity): """A class for entities using DataUpdateCoordinator.""" diff --git a/homeassistant/loader.py b/homeassistant/loader.py index ba29ff4a8da..215f552a908 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -20,11 +20,13 @@ from typing import ( List, Optional, Set, + TypedDict, TypeVar, Union, cast, ) +from homeassistant.generated.dhcp import DHCP from homeassistant.generated.mqtt import MQTT from homeassistant.generated.ssdp import SSDP from homeassistant.generated.zeroconf import HOMEKIT, ZEROCONF @@ -33,7 +35,11 @@ from homeassistant.generated.zeroconf import HOMEKIT, ZEROCONF if TYPE_CHECKING: from homeassistant.core import HomeAssistant -CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name +# mypy: disallow-any-generics + +CALLABLE_T = TypeVar( # pylint: disable=invalid-name + "CALLABLE_T", bound=Callable[..., Any] +) _LOGGER = logging.getLogger(__name__) @@ -53,12 +59,38 @@ _UNDEF = object() # Internal; not helpers.typing.UNDEFINED due to circular depe MAX_LOAD_CONCURRENTLY = 4 -def manifest_from_legacy_module(domain: str, module: ModuleType) -> Dict: +class Manifest(TypedDict, total=False): + """ + Integration manifest. + + Note that none of the attributes are marked Optional here. However, some of them may be optional in manifest.json + in the sense that they can be omitted altogether. But when present, they should not have null values in it. + """ + + name: str + disabled: str + domain: str + dependencies: List[str] + after_dependencies: List[str] + requirements: List[str] + config_flow: bool + documentation: str + issue_tracker: str + quality_scale: str + mqtt: List[str] + ssdp: List[Dict[str, str]] + zeroconf: List[Union[str, Dict[str, str]]] + dhcp: List[Dict[str, str]] + homekit: Dict[str, List[str]] + is_built_in: bool + codeowners: List[str] + + +def manifest_from_legacy_module(domain: str, module: ModuleType) -> Manifest: """Generate a manifest from a legacy module.""" return { "domain": domain, "name": domain, - "documentation": None, "requirements": getattr(module, "REQUIREMENTS", []), "dependencies": getattr(module, "DEPENDENCIES", []), "codeowners": [], @@ -171,6 +203,20 @@ async def async_get_zeroconf(hass: "HomeAssistant") -> Dict[str, List[Dict[str, return zeroconf +async def async_get_dhcp(hass: "HomeAssistant") -> List[Dict[str, str]]: + """Return cached list of dhcp types.""" + dhcp: List[Dict[str, str]] = DHCP.copy() + + integrations = await async_get_custom_components(hass) + for integration in integrations.values(): + if not integration.dhcp: + continue + for entry in integration.dhcp: + dhcp.append({"domain": integration.domain, **entry}) + + return dhcp + + async def async_get_homekit(hass: "HomeAssistant") -> Dict[str, str]: """Return cached list of homekit models.""" @@ -190,10 +236,10 @@ async def async_get_homekit(hass: "HomeAssistant") -> Dict[str, str]: return homekit -async def async_get_ssdp(hass: "HomeAssistant") -> Dict[str, List]: +async def async_get_ssdp(hass: "HomeAssistant") -> Dict[str, List[Dict[str, str]]]: """Return cached list of ssdp mappings.""" - ssdp: Dict[str, List] = SSDP.copy() + ssdp: Dict[str, List[Dict[str, str]]] = SSDP.copy() integrations = await async_get_custom_components(hass) for integration in integrations.values(): @@ -205,10 +251,10 @@ async def async_get_ssdp(hass: "HomeAssistant") -> Dict[str, List]: return ssdp -async def async_get_mqtt(hass: "HomeAssistant") -> Dict[str, List]: +async def async_get_mqtt(hass: "HomeAssistant") -> Dict[str, List[str]]: """Return cached list of MQTT mappings.""" - mqtt: Dict[str, List] = MQTT.copy() + mqtt: Dict[str, List[str]] = MQTT.copy() integrations = await async_get_custom_components(hass) for integration in integrations.values(): @@ -273,7 +319,7 @@ class Integration: hass: "HomeAssistant", pkg_path: str, file_path: pathlib.Path, - manifest: Dict[str, Any], + manifest: Manifest, ): """Initialize an integration.""" self.hass = hass @@ -294,72 +340,77 @@ class Integration: @property def name(self) -> str: """Return name.""" - return cast(str, self.manifest["name"]) + return self.manifest["name"] @property def disabled(self) -> Optional[str]: """Return reason integration is disabled.""" - return cast(Optional[str], self.manifest.get("disabled")) + return self.manifest.get("disabled") @property def domain(self) -> str: """Return domain.""" - return cast(str, self.manifest["domain"]) + return self.manifest["domain"] @property def dependencies(self) -> List[str]: """Return dependencies.""" - return cast(List[str], self.manifest.get("dependencies", [])) + return self.manifest.get("dependencies", []) @property def after_dependencies(self) -> List[str]: """Return after_dependencies.""" - return cast(List[str], self.manifest.get("after_dependencies", [])) + return self.manifest.get("after_dependencies", []) @property def requirements(self) -> List[str]: """Return requirements.""" - return cast(List[str], self.manifest.get("requirements", [])) + return self.manifest.get("requirements", []) @property def config_flow(self) -> bool: """Return config_flow.""" - return cast(bool, self.manifest.get("config_flow", False)) + return self.manifest.get("config_flow") or False @property def documentation(self) -> Optional[str]: """Return documentation.""" - return cast(str, self.manifest.get("documentation")) + return self.manifest.get("documentation") @property def issue_tracker(self) -> Optional[str]: """Return issue tracker link.""" - return cast(str, self.manifest.get("issue_tracker")) + return self.manifest.get("issue_tracker") @property def quality_scale(self) -> Optional[str]: """Return Integration Quality Scale.""" - return cast(str, self.manifest.get("quality_scale")) + return self.manifest.get("quality_scale") @property - def mqtt(self) -> Optional[list]: + def mqtt(self) -> Optional[List[str]]: """Return Integration MQTT entries.""" - return cast(List[dict], self.manifest.get("mqtt")) + return self.manifest.get("mqtt") @property - def ssdp(self) -> Optional[list]: + def ssdp(self) -> Optional[List[Dict[str, str]]]: """Return Integration SSDP entries.""" - return cast(List[dict], self.manifest.get("ssdp")) + return self.manifest.get("ssdp") @property - def zeroconf(self) -> Optional[list]: + def zeroconf(self) -> Optional[List[Union[str, Dict[str, str]]]]: """Return Integration zeroconf entries.""" - return cast(List[str], self.manifest.get("zeroconf")) + return self.manifest.get("zeroconf") @property - def homekit(self) -> Optional[dict]: + def dhcp(self) -> Optional[List[Dict[str, str]]]: + """Return Integration dhcp entries.""" + return self.manifest.get("dhcp") + + @property + def homekit(self) -> Optional[Dict[str, List[str]]]: """Return Integration homekit entries.""" - return cast(Dict[str, List], self.manifest.get("homekit")) + return self.manifest.get("homekit") @property def is_built_in(self) -> bool: diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index fa9a1ed5640..31a7414dc3c 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -6,27 +6,27 @@ astral==1.10.1 async_timeout==3.0.1 attrs==19.3.0 bcrypt==3.1.7 -certifi>=2020.6.20 +certifi>=2020.12.5 ciso8601==2.1.3 cryptography==3.2 defusedxml==0.6.0 distro==1.5.0 emoji==0.5.4 -hass-nabucasa==0.39.0 -home-assistant-frontend==20201229.1 +hass-nabucasa==0.41.0 +home-assistant-frontend==20210127.7 httpx==0.16.1 -importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 netdisco==2.8.2 paho-mqtt==1.5.1 -pillow==7.2.0 +pillow==8.1.0 pip>=8.0.3,<20.3 python-slugify==4.0.1 -pytz>=2020.1 -pyyaml==5.3.1 -requests==2.25.0 +pytz>=2020.5 +pyyaml==5.4.1 +requests==2.25.1 ruamel.yaml==0.15.100 -sqlalchemy==1.3.20 +scapy==2.4.4 +sqlalchemy==1.3.22 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.6.3 @@ -40,6 +40,10 @@ urllib3>=1.24.3 # Constrain H11 to ensure we get a new enough version to support non-rfc line endings h11>=0.12.0 +# Constrain httpcore to fix exception when connection dropped +# https://github.com/encode/httpcore/issues/239 +httpcore>=0.12.3 + # Constrain httplib2 to protect against CVE-2020-11078 httplib2>=0.18.0 diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index cebfd95591f..6ea2074ddf4 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -9,11 +9,14 @@ from homeassistant.helpers.typing import UNDEFINED, UndefinedType from homeassistant.loader import Integration, IntegrationNotFound, async_get_integration import homeassistant.util.package as pkg_util +# mypy: disallow-any-generics + DATA_PIP_LOCK = "pip_lock" DATA_PKG_CACHE = "pkg_cache" DATA_INTEGRATIONS_WITH_REQS = "integrations_with_reqs" CONSTRAINT_FILE = "package_constraints.txt" DISCOVERY_INTEGRATIONS: Dict[str, Iterable[str]] = { + "dhcp": ("dhcp",), "mqtt": ("mqtt",), "ssdp": ("ssdp",), "zeroconf": ("zeroconf", "homekit"), @@ -23,7 +26,7 @@ DISCOVERY_INTEGRATIONS: Dict[str, Iterable[str]] = { class RequirementsNotFound(HomeAssistantError): """Raised when a component is not found.""" - def __init__(self, domain: str, requirements: List) -> None: + def __init__(self, domain: str, requirements: List[str]) -> None: """Initialize a component not found error.""" super().__init__(f"Requirements for {domain} not found: {requirements}.") self.domain = domain @@ -123,7 +126,7 @@ async def async_process_requirements( if pkg_util.is_installed(req): continue - def _install(req: str, kwargs: Dict) -> bool: + def _install(req: str, kwargs: Dict[str, Any]) -> bool: """Install requirement.""" return pkg_util.install_package(req, **kwargs) diff --git a/homeassistant/runner.py b/homeassistant/runner.py index 0f8bb836da5..6f13a47be81 100644 --- a/homeassistant/runner.py +++ b/homeassistant/runner.py @@ -3,13 +3,14 @@ import asyncio from concurrent.futures import ThreadPoolExecutor import dataclasses import logging -import sys from typing import Any, Dict, Optional from homeassistant import bootstrap from homeassistant.core import callback from homeassistant.helpers.frame import warn_use +# mypy: disallow-any-generics + # # Python 3.8 has significantly less workers by default # than Python 3.7. In order to be consistent between @@ -41,14 +42,7 @@ class RuntimeConfig: open_ui: bool = False -# In Python 3.8+ proactor policy is the default on Windows -if sys.platform == "win32" and sys.version_info[:2] < (3, 8): - PolicyBase = asyncio.WindowsProactorEventLoopPolicy -else: - PolicyBase = asyncio.DefaultEventLoopPolicy - - -class HassEventLoopPolicy(PolicyBase): # type: ignore +class HassEventLoopPolicy(asyncio.DefaultEventLoopPolicy): # type: ignore[valid-type,misc] """Event loop policy for Home Assistant.""" def __init__(self, debug: bool) -> None: @@ -89,7 +83,7 @@ class HassEventLoopPolicy(PolicyBase): # type: ignore @callback -def _async_loop_exception_handler(_: Any, context: Dict) -> None: +def _async_loop_exception_handler(_: Any, context: Dict[str, Any]) -> None: """Handle all exception inside the core loop.""" kwargs = {} exception = context.get("exception") diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index b5745d533fb..07a6a54e402 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -17,7 +17,7 @@ import homeassistant.util.yaml.loader as yaml_loader # mypy: allow-untyped-calls, allow-untyped-defs -REQUIREMENTS = ("colorlog==4.5.0",) +REQUIREMENTS = ("colorlog==4.6.2",) _LOGGER = logging.getLogger(__name__) # pylint: disable=protected-access diff --git a/homeassistant/util/__init__.py b/homeassistant/util/__init__.py index d3178cb5ddd..281a7d5308c 100644 --- a/homeassistant/util/__init__.py +++ b/homeassistant/util/__init__.py @@ -22,6 +22,7 @@ from typing import ( import slugify as unicode_slug +from ..helpers.deprecation import deprecated_function from .dt import as_local, utcnow T = TypeVar("T") @@ -32,6 +33,27 @@ RE_SANITIZE_FILENAME = re.compile(r"(~|\.\.|/|\\)") RE_SANITIZE_PATH = re.compile(r"(~|\.(\.)+)") +def raise_if_invalid_filename(filename: str) -> None: + """ + Check if a filename is valid. + + Raises a ValueError if the filename is invalid. + """ + if RE_SANITIZE_FILENAME.sub("", filename) != filename: + raise ValueError(f"{filename} is not a safe filename") + + +def raise_if_invalid_path(path: str) -> None: + """ + Check if a path is valid. + + Raises a ValueError if the path is invalid. + """ + if RE_SANITIZE_PATH.sub("", path) != path: + raise ValueError(f"{path} is not a safe path") + + +@deprecated_function(replacement="raise_if_invalid_filename") def sanitize_filename(filename: str) -> str: """Check if a filename is safe. @@ -47,6 +69,7 @@ def sanitize_filename(filename: str) -> str: return filename +@deprecated_function(replacement="raise_if_invalid_path") def sanitize_path(path: str) -> str: """Check if a path is safe. @@ -64,7 +87,10 @@ def sanitize_path(path: str) -> str: def slugify(text: str, *, separator: str = "_") -> str: """Slugify a given text.""" - return unicode_slug.slugify(text, separator=separator) + if text == "": + return "" + slug = unicode_slug.slugify(text, separator=separator) + return "unknown" if slug == "" else slug def repr_helper(inp: Any) -> str: diff --git a/homeassistant/util/async_.py b/homeassistant/util/async_.py index ded44473038..f61225502ee 100644 --- a/homeassistant/util/async_.py +++ b/homeassistant/util/async_.py @@ -10,6 +10,8 @@ from typing import Any, Awaitable, Callable, Coroutine, TypeVar _LOGGER = logging.getLogger(__name__) +_SHUTDOWN_RUN_CALLBACK_THREADSAFE = "_shutdown_run_callback_threadsafe" + T = TypeVar("T") @@ -58,6 +60,28 @@ def run_callback_threadsafe( _LOGGER.warning("Exception on lost future: ", exc_info=True) loop.call_soon_threadsafe(run_callback) + + if hasattr(loop, _SHUTDOWN_RUN_CALLBACK_THREADSAFE): + # + # If the final `HomeAssistant.async_block_till_done` in + # `HomeAssistant.async_stop` has already been called, the callback + # will never run and, `future.result()` will block forever which + # will prevent the thread running this code from shutting down which + # will result in a deadlock when the main thread attempts to shutdown + # the executor and `.join()` the thread running this code. + # + # To prevent this deadlock we do the following on shutdown: + # + # 1. Set the _SHUTDOWN_RUN_CALLBACK_THREADSAFE attr on this function + # by calling `shutdown_run_callback_threadsafe` + # 2. Call `hass.async_block_till_done` at least once after shutdown + # to ensure all callbacks have run + # 3. Raise an exception here to ensure `future.result()` can never be + # called and hit the deadlock since once `shutdown_run_callback_threadsafe` + # we cannot promise the callback will be executed. + # + raise RuntimeError("The event loop is in the process of shutting down.") + return future @@ -139,3 +163,20 @@ async def gather_with_concurrency( return await gather( *(sem_task(task) for task in tasks), return_exceptions=return_exceptions ) + + +def shutdown_run_callback_threadsafe(loop: AbstractEventLoop) -> None: + """Call when run_callback_threadsafe should prevent creating new futures. + + We must finish all callbacks before the executor is shutdown + or we can end up in a deadlock state where: + + `executor.result()` is waiting for its `._condition` + and the executor shutdown is trying to `.join()` the + executor thread. + + This function is considered irreversible and should only ever + be called when Home Assistant is going to shutdown and + python is going to exit. + """ + setattr(loop, _SHUTDOWN_RUN_CALLBACK_THREADSAFE, True) diff --git a/homeassistant/util/logging.py b/homeassistant/util/logging.py index 2f202165550..feef339a200 100644 --- a/homeassistant/util/logging.py +++ b/homeassistant/util/logging.py @@ -34,8 +34,6 @@ class HomeAssistantQueueHandler(logging.handlers.QueueHandler): """Emit a log record.""" try: self.enqueue(record) - except asyncio.CancelledError: - raise except Exception: # pylint: disable=broad-except self.handleError(record) diff --git a/homeassistant/util/package.py b/homeassistant/util/package.py index a665fd78914..5391d92ed89 100644 --- a/homeassistant/util/package.py +++ b/homeassistant/util/package.py @@ -1,5 +1,6 @@ """Helpers to install PyPi packages.""" import asyncio +from importlib.metadata import PackageNotFoundError, version import logging import os from pathlib import Path @@ -10,17 +11,6 @@ from urllib.parse import urlparse import pkg_resources -if sys.version_info[:2] >= (3, 8): - from importlib.metadata import ( # pylint: disable=no-name-in-module,import-error - PackageNotFoundError, - version, - ) -else: - from importlib_metadata import ( # pylint: disable=import-error - PackageNotFoundError, - version, - ) - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/util/process.py b/homeassistant/util/process.py index fb2d6dec58e..6f8bafda7a7 100644 --- a/homeassistant/util/process.py +++ b/homeassistant/util/process.py @@ -1,9 +1,17 @@ """Util to handle processes.""" +from __future__ import annotations + import subprocess +from typing import Any + +# mypy: disallow-any-generics -def kill_subprocess(process: subprocess.Popen) -> None: +def kill_subprocess( + # pylint: disable=unsubscriptable-object # https://github.com/PyCQA/pylint/issues/4034 + process: subprocess.Popen[Any], +) -> None: """Force kill a subprocess and wait for it to exit.""" process.kill() process.communicate() diff --git a/homeassistant/util/thread.py b/homeassistant/util/thread.py index bf61c67172a..7743e1d159c 100644 --- a/homeassistant/util/thread.py +++ b/homeassistant/util/thread.py @@ -1,33 +1,10 @@ """Threading util helpers.""" import ctypes import inspect -import sys import threading from typing import Any -def fix_threading_exception_logging() -> None: - """Fix threads passing uncaught exceptions to our exception hook. - - https://bugs.python.org/issue1230540 - Fixed in Python 3.8. - """ - if sys.version_info[:2] >= (3, 8): - return - - run_old = threading.Thread.run - - def run(*args: Any, **kwargs: Any) -> None: - try: - run_old(*args, **kwargs) - except (KeyboardInterrupt, SystemExit): # pylint: disable=try-except-raise - raise - except Exception: # pylint: disable=broad-except - sys.excepthook(*sys.exc_info()) - - threading.Thread.run = run # type: ignore - - def _async_raise(tid: int, exctype: Any) -> None: """Raise an exception in the threads with id tid.""" if not inspect.isclass(exctype): diff --git a/homeassistant/util/timeout.py b/homeassistant/util/timeout.py index 8c3d38a3700..d8fc3e48fe6 100644 --- a/homeassistant/util/timeout.py +++ b/homeassistant/util/timeout.py @@ -49,14 +49,14 @@ class _GlobalFreezeContext: self._loop.call_soon_threadsafe(self._enter) return self - def __exit__( + def __exit__( # pylint: disable=useless-return self, exc_type: Type[BaseException], exc_val: BaseException, exc_tb: TracebackType, ) -> Optional[bool]: self._loop.call_soon_threadsafe(self._exit) - return True + return None def _enter(self) -> None: """Run freeze.""" @@ -117,14 +117,14 @@ class _ZoneFreezeContext: self._loop.call_soon_threadsafe(self._enter) return self - def __exit__( + def __exit__( # pylint: disable=useless-return self, exc_type: Type[BaseException], exc_val: BaseException, exc_tb: TracebackType, ) -> Optional[bool]: self._loop.call_soon_threadsafe(self._exit) - return True + return None def _enter(self) -> None: """Run freeze.""" @@ -347,6 +347,10 @@ class _ZoneTimeoutManager: self._tasks: List[_ZoneTaskContext] = [] self._freezes: List[_ZoneFreezeContext] = [] + def __repr__(self) -> str: + """Representation of a zone.""" + return f"<{self.name}: {len(self._tasks)} / {len(self._freezes)}>" + @property def name(self) -> str: """Return Zone name.""" diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py index 9e0d0c2c979..5cba1bfeb19 100644 --- a/homeassistant/util/unit_system.py +++ b/homeassistant/util/unit_system.py @@ -1,6 +1,6 @@ """Unit system helper class and methods.""" from numbers import Number -from typing import Optional +from typing import Dict, Optional from homeassistant.const import ( CONF_UNIT_SYSTEM_IMPERIAL, @@ -31,6 +31,8 @@ from homeassistant.util import ( volume as volume_util, ) +# mypy: disallow-any-generics + LENGTH_UNITS = distance_util.VALID_UNITS MASS_UNITS = [MASS_POUNDS, MASS_OUNCES, MASS_KILOGRAMS, MASS_GRAMS] @@ -135,7 +137,7 @@ class UnitSystem: # type ignore: https://github.com/python/mypy/issues/7207 return volume_util.convert(volume, from_unit, self.volume_unit) # type: ignore - def as_dict(self) -> dict: + def as_dict(self) -> Dict[str, str]: """Convert the unit system to a dictionary.""" return { LENGTH: self.length_unit, diff --git a/pyproject.toml b/pyproject.toml index 0f416d9e014..445f13e8724 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.black] -target-version = ["py37", "py38"] +target-version = ["py38"] exclude = 'generated' [tool.isort] diff --git a/requirements.txt b/requirements.txt index ece5a5a3709..c973f4e4030 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,18 +6,17 @@ astral==1.10.1 async_timeout==3.0.1 attrs==19.3.0 bcrypt==3.1.7 -certifi>=2020.6.20 +certifi>=2020.12.5 ciso8601==2.1.3 httpx==0.16.1 -importlib-metadata==1.6.0;python_version<'3.8' jinja2>=2.11.2 PyJWT==1.7.1 cryptography==3.2 pip>=8.0.3,<20.3 python-slugify==4.0.1 -pytz>=2020.1 -pyyaml==5.3.1 -requests==2.25.0 +pytz>=2020.5 +pyyaml==5.4.1 +requests==2.25.1 ruamel.yaml==0.15.100 voluptuous==0.12.1 voluptuous-serialize==2.4.0 diff --git a/requirements_all.txt b/requirements_all.txt index 8a5d1a61828..309b77acfe9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -49,7 +49,7 @@ PyNaCl==1.3.0 PyQRCode==1.2.1 # homeassistant.components.rmvtransport -PyRMVtransport==0.2.9 +PyRMVtransport==0.2.10 # homeassistant.components.telegram_bot PySocks==1.7.1 @@ -64,7 +64,7 @@ PyTransportNSW==0.1.1 PyTurboJPEG==1.4.0 # homeassistant.components.vicare -PyViCare==0.2.0 +PyViCare==0.2.5 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.13.4 @@ -81,7 +81,7 @@ RtmAPI==0.7.2 TravisPy==0.3.5 # homeassistant.components.twitter -TwitterAPI==2.6.2.1 +TwitterAPI==2.6.3 # homeassistant.components.tof # VL53L1X2==0.1.5 @@ -154,7 +154,7 @@ aiodns==2.0.0 aioeafm==0.1.2 # homeassistant.components.esphome -aioesphomeapi==2.6.3 +aioesphomeapi==2.6.4 # homeassistant.components.flo aioflo==0.4.1 @@ -191,11 +191,14 @@ aiokafka==0.6.0 aiokef==0.2.16 # homeassistant.components.lifx -aiolifx==0.6.7 +aiolifx==0.6.9 # homeassistant.components.lifx aiolifx_effects==0.2.2 +# homeassistant.components.lutron_caseta +aiolip==1.0.1 + # homeassistant.components.keyboard_remote aionotify==0.2.0 @@ -218,7 +221,7 @@ aiopylgtv==0.3.3 aiorecollect==1.0.1 # homeassistant.components.shelly -aioshelly==0.5.1 +aioshelly==0.5.3 # homeassistant.components.switcher_kis aioswitcher==1.2.1 @@ -236,7 +239,7 @@ airly==1.0.0 aladdin_connect==0.3 # homeassistant.components.alpha_vantage -alpha_vantage==2.2.0 +alpha_vantage==2.3.1 # homeassistant.components.ambiclimate ambiclimate==0.2.1 @@ -306,7 +309,7 @@ av==8.0.2 # avion==0.10 # homeassistant.components.axis -axis==41 +axis==43 # homeassistant.components.azure_event_hub azure-eventhub==5.1.0 @@ -404,7 +407,7 @@ btsmarthub_devicelist==0.2.0 buienradar==1.0.4 # homeassistant.components.caldav -caldav==0.6.1 +caldav==0.7.1 # homeassistant.components.circuit circuit-webhook==1.0.1 @@ -428,7 +431,7 @@ coinbase==2.1.0 coinmarketcap==5.0.3 # homeassistant.scripts.check_config -colorlog==4.5.0 +colorlog==4.6.2 # homeassistant.components.color_extractor colorthief==0.2.1 @@ -460,7 +463,7 @@ datadog==0.15.0 datapoint==0.9.5 # homeassistant.components.debugpy -debugpy==1.2.0 +debugpy==1.2.1 # homeassistant.components.decora # decora==0.6 @@ -478,7 +481,7 @@ defusedxml==0.6.0 deluge-client==1.7.1 # homeassistant.components.denonavr -denonavr==0.9.9 +denonavr==0.9.10 # homeassistant.components.devolo_home_control devolo-home-control-api==0.16.0 @@ -490,7 +493,7 @@ directv==0.4.0 discogs_client==2.3.0 # homeassistant.components.discord -discord.py==1.5.1 +discord.py==1.6.0 # homeassistant.components.updater distro==1.5.0 @@ -535,7 +538,7 @@ elgato==1.0.0 eliqonline==1.2.2 # homeassistant.components.elkm1 -elkm1-lib==0.8.8 +elkm1-lib==0.8.10 # homeassistant.components.mobile_app emoji==0.5.4 @@ -584,7 +587,7 @@ evohome-async==0.3.5.post1 fastdotcom==0.0.3 # homeassistant.components.feedreader -feedparser-homeassistant==5.2.2.dev1 +feedparser==6.0.2 # homeassistant.components.fibaro fiblary3==0.1.7 @@ -669,7 +672,7 @@ gntp==1.0.3 goalzero==0.1.4 # homeassistant.components.gogogate2 -gogogate2-api==2.0.3 +gogogate2-api==3.0.0 # homeassistant.components.google google-api-python-client==1.6.4 @@ -681,7 +684,7 @@ google-cloud-pubsub==2.1.0 google-cloud-texttospeech==0.4.0 # homeassistant.components.nest -google-nest-sdm==0.2.5 +google-nest-sdm==0.2.9 # homeassistant.components.google_travel_time googlemaps==2.5.1 @@ -729,13 +732,13 @@ habitipy==0.2.0 hangups==0.4.11 # homeassistant.components.cloud -hass-nabucasa==0.39.0 +hass-nabucasa==0.41.0 # homeassistant.components.splunk hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.1.6 +hatasmota==0.2.7 # homeassistant.components.jewish_calendar hdate==0.9.12 @@ -762,7 +765,7 @@ hole==0.5.1 holidays==0.10.4 # homeassistant.components.frontend -home-assistant-frontend==20201229.1 +home-assistant-frontend==20210127.7 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 @@ -771,23 +774,26 @@ homeassistant-pyozw==0.1.10 homeconnect==0.6.3 # homeassistant.components.homematicip_cloud -homematicip==0.13.0 +homematicip==0.13.1 # homeassistant.components.horizon horimote==0.4.1 # homeassistant.components.google # homeassistant.components.remember_the_milk -httplib2==0.10.3 +httplib2==0.18.1 # homeassistant.components.huawei_lte huawei-lte-api==1.4.17 +# homeassistant.components.huisbaasje +huisbaasje-client==0.1.0 + # homeassistant.components.hydrawise hydrawiser==0.2 # homeassistant.components.hyperion -hyperion-py==0.6.1 +hyperion-py==0.7.0 # homeassistant.components.bh1750 # homeassistant.components.bme280 @@ -874,7 +880,7 @@ life360==4.1.1 liffylights==0.9.4 # homeassistant.components.osramlightify -lightify==1.0.7.2 +lightify==1.0.7.3 # homeassistant.components.lightwave lightwave==0.19 @@ -889,7 +895,7 @@ linode-api==4.1.9b1 lmnotify==0.0.4 # homeassistant.components.google_maps -locationsharinglib==4.1.0 +locationsharinglib==4.1.5 # homeassistant.components.logi_circle logi_circle==0.2.2 @@ -916,7 +922,7 @@ magicseaweed==1.0.3 matrix-client==0.3.2 # homeassistant.components.maxcube -maxcube-api==0.1.0 +maxcube-api==0.3.0 # homeassistant.components.mythicbeastsdns mbddns==0.1.2 @@ -949,13 +955,13 @@ minio==4.0.9 mitemp_bt==0.0.3 # homeassistant.components.motion_blinds -motionblinds==0.4.7 +motionblinds==0.4.8 # homeassistant.components.tts mutagen==1.45.1 # homeassistant.components.mychevy -mychevy==2.0.1 +mychevy==2.1.1 # homeassistant.components.mycroft mycroftapi==2.0 @@ -983,7 +989,7 @@ netdisco==2.8.2 neurio==0.3.1 # homeassistant.components.nexia -nexia==0.9.4 +nexia==0.9.5 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 @@ -1033,6 +1039,9 @@ oemthermostat==1.1.1 # homeassistant.components.omnilogic omnilogic==0.4.2 +# homeassistant.components.ondilo_ico +ondilo==0.2.0 + # homeassistant.components.onkyo onkyo-eiscp==1.2.7 @@ -1122,13 +1131,13 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==7.2.0 +pillow==8.1.0 # homeassistant.components.dominos pizzapi==0.0.3 # homeassistant.components.plex -plexapi==4.2.0 +plexapi==4.3.1 # homeassistant.components.plex plexauth==0.0.6 @@ -1137,7 +1146,7 @@ plexauth==0.0.6 plexwebsocket==0.0.12 # homeassistant.components.plugwise -plugwise==0.8.3 +plugwise==0.8.5 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 @@ -1170,11 +1179,8 @@ proxmoxer==1.1.1 # homeassistant.components.systemmonitor psutil==5.8.0 -# homeassistant.components.ptvsd -ptvsd==4.3.2 - # homeassistant.components.wink -pubnubsub-handler==1.0.8 +pubnubsub-handler==1.0.9 # homeassistant.components.pulseaudio_loopback pulsectl==20.2.4 @@ -1232,13 +1238,13 @@ pyHS100==0.3.5.2 pyMetno==0.8.1 # homeassistant.components.rfxtrx -pyRFXtrx==0.26 +pyRFXtrx==0.26.1 # homeassistant.components.switchmate # pySwitchmate==0.4.6 # homeassistant.components.tibber -pyTibber==0.16.0 +pyTibber==0.16.1 # homeassistant.components.dlink pyW215==0.7.0 @@ -1258,6 +1264,9 @@ pyaehw4a1==0.3.9 # homeassistant.components.aftership pyaftership==0.1.2 +# homeassistant.components.airnow +pyairnow==1.1.0 + # homeassistant.components.airvisual pyairvisual==5.0.4 @@ -1277,7 +1286,7 @@ pyatmo==4.2.2 pyatome==0.1.1 # homeassistant.components.apple_tv -pyatv==0.7.5 +pyatv==0.7.6 # homeassistant.components.bbox pybbox==0.0.5-alpha @@ -1301,7 +1310,7 @@ pycfdns==1.2.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==7.6.0 +pychromecast==8.0.0 # homeassistant.components.pocketcasts pycketcasts==1.0.0 @@ -1313,7 +1322,7 @@ pycmus==0.1.1 pycocotools==2.0.1 # homeassistant.components.comfoconnect -pycomfoconnect==0.3 +pycomfoconnect==0.4 # homeassistant.components.coolmaster pycoolmasternet-async==0.1.2 @@ -1325,7 +1334,7 @@ pycsspeechtts==1.0.4 # pycups==1.9.73 # homeassistant.components.daikin -pydaikin==2.4.0 +pydaikin==2.4.1 # homeassistant.components.danfoss_air pydanfossair==0.1.0 @@ -1352,7 +1361,7 @@ pydroid-ipcam==0.8 pyebox==1.1.4 # homeassistant.components.econet -pyeconet==0.0.11 +pyeconet==0.1.12 # homeassistant.components.edimax pyedimax==0.2.1 @@ -1428,7 +1437,7 @@ pyhik==0.2.8 pyhiveapi==0.2.20.2 # homeassistant.components.homematic -pyhomematic==0.1.70 +pyhomematic==0.1.71 # homeassistant.components.homeworks pyhomeworks==0.0.6 @@ -1479,7 +1488,7 @@ pykwb==0.0.8 pylacrosse==0.4 # homeassistant.components.lastfm -pylast==4.0.0 +pylast==4.1.0 # homeassistant.components.launch_library pylaunches==1.0.0 @@ -1497,7 +1506,7 @@ pylitejet==0.1 pyloopenergy==0.2.1 # homeassistant.components.lutron_caseta -pylutron-caseta==0.7.2 +pylutron-caseta==0.9.0 # homeassistant.components.lutron pylutron==0.2.5 @@ -1569,7 +1578,7 @@ pynx584==0.5 pynzbgetapi==0.2.0 # homeassistant.components.obihai -pyobihai==1.2.3 +pyobihai==1.3.1 # homeassistant.components.ombi pyombi==0.1.10 @@ -1595,7 +1604,7 @@ pyotgw==1.0b1 pyotp==2.3.0 # homeassistant.components.openweathermap -pyowm==3.1.0 +pyowm==3.1.1 # homeassistant.components.onewire pyownet==0.10.0.post1 @@ -1604,7 +1613,7 @@ pyownet==0.10.0.post1 pypca==0.0.7 # homeassistant.components.lcn -pypck==0.7.7 +pypck==0.7.9 # homeassistant.components.pjlink pypjlink2==1.2.1 @@ -1678,7 +1687,7 @@ pyskyqhub==0.1.3 pysma==0.3.5 # homeassistant.components.smappee -pysmappee==0.2.13 +pysmappee==0.2.16 # homeassistant.components.smartthings pysmartapp==0.3.3 @@ -1765,7 +1774,7 @@ python-gitlab==1.6.0 python-hpilo==4.3 # homeassistant.components.izone -python-izone==1.1.2 +python-izone==1.1.3 # homeassistant.components.joaoapps_join python-join-api==0.0.6 @@ -1807,7 +1816,7 @@ python-sochain-api==0.0.2 python-songpal==0.12 # homeassistant.components.tado -python-tado==0.8.1 +python-tado==0.10.0 # homeassistant.components.telegram_bot python-telegram-bot==13.1 @@ -1819,7 +1828,7 @@ python-telnet-vlc==1.0.4 python-twitch-client==0.6.0 # homeassistant.components.velbus -python-velbus==2.1.1 +python-velbus==2.1.2 # homeassistant.components.vlc python-vlc==1.1.2 @@ -1840,7 +1849,7 @@ python_opendata_transport==0.2.1 pythonegardia==1.0.40 # homeassistant.components.tile -pytile==4.0.0 +pytile==5.1.0 # homeassistant.components.touchline pytouchline==0.7 @@ -1886,10 +1895,10 @@ pyvolumio==0.1.3 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.5.6 +pywemo==0.6.1 # homeassistant.components.wilight -pywilight==0.0.65 +pywilight==0.0.66 # homeassistant.components.xeoma pyxeoma==1.4.1 @@ -1910,7 +1919,7 @@ quantum-gateway==0.0.5 rachiopy==1.0.3 # homeassistant.components.radiotherm -radiotherm==2.0.0 +radiotherm==2.1.0 # homeassistant.components.raincloud raincloudy==0.0.7 @@ -1952,7 +1961,7 @@ rokuecp==0.6.0 roombapy==1.6.2 # homeassistant.components.roon -roonapi==0.0.28 +roonapi==0.0.31 # homeassistant.components.rova rova==0.1.0 @@ -1981,6 +1990,9 @@ samsungtvws==1.4.0 # homeassistant.components.satel_integra satel_integra==0.3.4 +# homeassistant.components.dhcp +scapy==2.4.4 + # homeassistant.components.deutsche_bahn schiene==0.23 @@ -1988,7 +2000,7 @@ schiene==0.23 scsgate==0.1.0 # homeassistant.components.sendgrid -sendgrid==6.4.6 +sendgrid==6.5.0 # homeassistant.components.sensehat sense-hat==2.2.0 @@ -1998,7 +2010,7 @@ sense-hat==2.2.0 sense_energy==0.8.1 # homeassistant.components.sentry -sentry-sdk==0.19.4 +sentry-sdk==0.19.5 # homeassistant.components.sharkiq sharkiqpy==0.1.8 @@ -2007,7 +2019,7 @@ sharkiqpy==0.1.8 sharp_aquos_rc==0.3.2 # homeassistant.components.shodan -shodan==1.24.0 +shodan==1.25.0 # homeassistant.components.sighthound simplehound==0.3 @@ -2016,7 +2028,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==9.6.2 +simplisafe-python==9.6.4 # homeassistant.components.sisyphus sisyphus-control==3.0 @@ -2031,7 +2043,7 @@ slackclient==2.5.0 sleepyq==0.8.1 # homeassistant.components.xmpp -slixmpp==1.5.2 +slixmpp==1.6.0 # homeassistant.components.smart_meter_texas smart-meter-texas==0.4.0 @@ -2091,13 +2103,13 @@ spotipy==2.16.1 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.20 +sqlalchemy==1.3.22 # homeassistant.components.srp_energy srpenergy==1.3.2 # homeassistant.components.starline -starline==0.1.3 +starline==0.1.5 # homeassistant.components.starlingbank starlingbank==3.2 @@ -2127,7 +2139,7 @@ sucks==0.9.4 sunwatcher==0.2.1 # homeassistant.components.surepetcare -surepy==0.2.6 +surepy==0.4.0 # homeassistant.components.swiss_hydrological_data swisshydrodata==0.0.3 @@ -2220,7 +2232,7 @@ uEagle==0.0.2 unifiled==0.11 # homeassistant.components.upb -upb_lib==0.4.11 +upb_lib==0.4.12 # homeassistant.components.upcloud upcloud-api==0.4.5 @@ -2245,13 +2257,13 @@ venstarcolortouch==0.13 vilfo-api-client==0.3.2 # homeassistant.components.volkszaehler -volkszaehler==0.1.3 +volkszaehler==0.2.1 # homeassistant.components.volvooncall volvooncall==0.8.12 # homeassistant.components.verisure -vsure==1.5.4 +vsure==1.6.1 # homeassistant.components.vasttrafik vtjp==0.1.14 @@ -2305,7 +2317,7 @@ xboxapi==2.0.1 xfinity-gateway==0.0.4 # homeassistant.components.knx -xknx==0.15.6 +xknx==0.16.2 # homeassistant.components.bluesound # homeassistant.components.rest @@ -2327,7 +2339,7 @@ yeelight==0.5.4 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2020.12.29 +youtube_dl==2021.01.16 # homeassistant.components.onvif zeep[async]==4.0.0 @@ -2339,7 +2351,7 @@ zengge==0.2 zeroconf==0.28.8 # homeassistant.components.zha -zha-quirks==0.0.51 +zha-quirks==0.0.53 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2363,7 +2375,10 @@ zigpy-zigate==0.7.3 zigpy-znp==0.3.0 # homeassistant.components.zha -zigpy==0.30.0 +zigpy==0.32.0 # homeassistant.components.zoneminder zm-py==0.5.2 + +# homeassistant.components.zwave_js +zwave-js-server-python==0.17.0 diff --git a/requirements_test.txt b/requirements_test.txt index e4553a2498b..380240e3ffc 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -4,16 +4,16 @@ -c homeassistant/package_constraints.txt -r requirements_test_pre_commit.txt -asynctest==0.13.0 codecov==2.1.10 -coverage==5.3 +coverage==5.4 jsonpickle==1.4.1 mock-open==1.4.0 mypy==0.790 -pre-commit==2.9.2 +pre-commit==2.9.3 pylint==2.6.0 astroid==2.4.2 pipdeptree==1.0.0 +awesomeversion==21.2.0 pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 pytest-cov==2.10.1 @@ -21,7 +21,7 @@ pytest-test-groups==1.0.3 pytest-sugar==0.9.4 pytest-timeout==1.4.2 pytest-xdist==2.1.0 -pytest==6.1.2 +pytest==6.2.2 requests_mock==1.8.0 responses==0.12.0 respx==0.16.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5bbfd1e5d1e..cd388ff403c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -18,7 +18,7 @@ PyNaCl==1.3.0 PyQRCode==1.2.1 # homeassistant.components.rmvtransport -PyRMVtransport==0.2.9 +PyRMVtransport==0.2.10 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 @@ -88,7 +88,7 @@ aiodns==2.0.0 aioeafm==0.1.2 # homeassistant.components.esphome -aioesphomeapi==2.6.3 +aioesphomeapi==2.6.4 # homeassistant.components.flo aioflo==0.4.1 @@ -115,6 +115,9 @@ aiohue==2.1.0 # homeassistant.components.apache_kafka aiokafka==0.6.0 +# homeassistant.components.lutron_caseta +aiolip==1.0.1 + # homeassistant.components.notion aionotion==1.1.0 @@ -134,7 +137,7 @@ aiopylgtv==0.3.3 aiorecollect==1.0.1 # homeassistant.components.shelly -aioshelly==0.5.1 +aioshelly==0.5.3 # homeassistant.components.switcher_kis aioswitcher==1.2.1 @@ -177,7 +180,7 @@ auroranoaa==0.0.2 av==8.0.2 # homeassistant.components.axis -axis==41 +axis==43 # homeassistant.components.azure_event_hub azure-eventhub==5.1.0 @@ -216,13 +219,13 @@ bsblan==0.4.0 buienradar==1.0.4 # homeassistant.components.caldav -caldav==0.6.1 +caldav==0.7.1 # homeassistant.components.coinmarketcap coinmarketcap==5.0.3 # homeassistant.scripts.check_config -colorlog==4.5.0 +colorlog==4.6.2 # homeassistant.components.color_extractor colorthief==0.2.1 @@ -245,7 +248,7 @@ datadog==0.15.0 datapoint==0.9.5 # homeassistant.components.debugpy -debugpy==1.2.0 +debugpy==1.2.1 # homeassistant.components.ihc # homeassistant.components.namecheapdns @@ -254,7 +257,7 @@ debugpy==1.2.0 defusedxml==0.6.0 # homeassistant.components.denonavr -denonavr==0.9.9 +denonavr==0.9.10 # homeassistant.components.devolo_home_control devolo-home-control-api==0.16.0 @@ -281,7 +284,7 @@ eebrightbox==0.0.4 elgato==1.0.0 # homeassistant.components.elkm1 -elkm1-lib==0.8.8 +elkm1-lib==0.8.10 # homeassistant.components.mobile_app emoji==0.5.4 @@ -299,7 +302,7 @@ ephem==3.7.7.0 epson-projector==0.2.3 # homeassistant.components.feedreader -feedparser-homeassistant==5.2.2.dev1 +feedparser==6.0.2 # homeassistant.components.homekit fnvhash==0.1.0 @@ -307,6 +310,11 @@ fnvhash==0.1.0 # homeassistant.components.foobot foobot_async==1.0.0 +# homeassistant.components.fritz +# homeassistant.components.fritzbox_callmonitor +# homeassistant.components.fritzbox_netmonitor +fritzconnection==1.4.0 + # homeassistant.components.google_translate gTTS==2.2.1 @@ -346,7 +354,7 @@ glances_api==0.2.0 goalzero==0.1.4 # homeassistant.components.gogogate2 -gogogate2-api==2.0.3 +gogogate2-api==3.0.0 # homeassistant.components.google google-api-python-client==1.6.4 @@ -355,7 +363,7 @@ google-api-python-client==1.6.4 google-cloud-pubsub==2.1.0 # homeassistant.components.nest -google-nest-sdm==0.2.5 +google-nest-sdm==0.2.9 # homeassistant.components.gree greeclimate==0.10.3 @@ -373,10 +381,10 @@ ha-ffmpeg==3.0.2 hangups==0.4.11 # homeassistant.components.cloud -hass-nabucasa==0.39.0 +hass-nabucasa==0.41.0 # homeassistant.components.tasmota -hatasmota==0.1.6 +hatasmota==0.2.7 # homeassistant.components.jewish_calendar hdate==0.9.12 @@ -394,7 +402,7 @@ hole==0.5.1 holidays==0.10.4 # homeassistant.components.frontend -home-assistant-frontend==20201229.1 +home-assistant-frontend==20210127.7 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 @@ -403,17 +411,20 @@ homeassistant-pyozw==0.1.10 homeconnect==0.6.3 # homeassistant.components.homematicip_cloud -homematicip==0.13.0 +homematicip==0.13.1 # homeassistant.components.google # homeassistant.components.remember_the_milk -httplib2==0.10.3 +httplib2==0.18.1 # homeassistant.components.huawei_lte huawei-lte-api==1.4.17 +# homeassistant.components.huisbaasje +huisbaasje-client==0.1.0 + # homeassistant.components.hyperion -hyperion-py==0.6.1 +hyperion-py==0.7.0 # homeassistant.components.iaqualink iaqualink==0.3.4 @@ -443,6 +454,9 @@ konnected==1.2.0 # homeassistant.components.dyson libpurecool==0.6.4 +# homeassistant.components.foscam +libpyfoscam==1.0 + # homeassistant.components.mikrotik librouteros==3.0.0 @@ -474,7 +488,7 @@ millheater==0.4.0 minio==4.0.9 # homeassistant.components.motion_blinds -motionblinds==0.4.7 +motionblinds==0.4.8 # homeassistant.components.tts mutagen==1.45.1 @@ -487,7 +501,7 @@ nessclient==0.9.15 netdisco==2.8.2 # homeassistant.components.nexia -nexia==0.9.4 +nexia==0.9.5 # homeassistant.components.nsw_fuel_station nsw-fuel-api-client==1.0.10 @@ -513,6 +527,9 @@ objgraph==3.4.1 # homeassistant.components.omnilogic omnilogic==0.4.2 +# homeassistant.components.ondilo_ico +ondilo==0.2.0 + # homeassistant.components.onvif onvif-zeep-async==1.0.0 @@ -551,10 +568,10 @@ pilight==0.1.1 # homeassistant.components.seven_segments # homeassistant.components.sighthound # homeassistant.components.tensorflow -pillow==7.2.0 +pillow==8.1.0 # homeassistant.components.plex -plexapi==4.2.0 +plexapi==4.3.1 # homeassistant.components.plex plexauth==0.0.6 @@ -563,7 +580,7 @@ plexauth==0.0.6 plexwebsocket==0.0.12 # homeassistant.components.plugwise -plugwise==0.8.3 +plugwise==0.8.5 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 @@ -587,9 +604,6 @@ progettihwsw==0.1.1 # homeassistant.components.prometheus prometheus_client==0.7.1 -# homeassistant.components.ptvsd -ptvsd==4.3.2 - # homeassistant.components.androidtv pure-python-adb[async]==0.3.0.dev0 @@ -622,10 +636,10 @@ pyHS100==0.3.5.2 pyMetno==0.8.1 # homeassistant.components.rfxtrx -pyRFXtrx==0.26 +pyRFXtrx==0.26.1 # homeassistant.components.tibber -pyTibber==0.16.0 +pyTibber==0.16.1 # homeassistant.components.nextbus py_nextbusnext==0.1.4 @@ -633,6 +647,9 @@ py_nextbusnext==0.1.4 # homeassistant.components.hisense_aehw4a1 pyaehw4a1==0.3.9 +# homeassistant.components.airnow +pyairnow==1.1.0 + # homeassistant.components.airvisual pyairvisual==5.0.4 @@ -649,7 +666,7 @@ pyatag==0.3.4.4 pyatmo==4.2.2 # homeassistant.components.apple_tv -pyatv==0.7.5 +pyatv==0.7.6 # homeassistant.components.blackbird pyblackbird==0.5 @@ -661,13 +678,16 @@ pybotvac==0.0.20 pycfdns==1.2.1 # homeassistant.components.cast -pychromecast==7.6.0 +pychromecast==8.0.0 + +# homeassistant.components.comfoconnect +pycomfoconnect==0.4 # homeassistant.components.coolmaster pycoolmasternet-async==0.1.2 # homeassistant.components.daikin -pydaikin==2.4.0 +pydaikin==2.4.1 # homeassistant.components.deconz pydeconz==77 @@ -678,6 +698,9 @@ pydexcom==0.2.0 # homeassistant.components.zwave pydispatcher==2.0.5 +# homeassistant.components.econet +pyeconet==0.1.12 + # homeassistant.components.everlights pyeverlights==0.1.0 @@ -716,7 +739,7 @@ pyhaversion==3.4.2 pyheos==0.7.2 # homeassistant.components.homematic -pyhomematic==0.1.70 +pyhomematic==0.1.71 # homeassistant.components.icloud pyicloud==0.9.7 @@ -746,7 +769,7 @@ pykodi==0.2.1 pykulersky==0.4.0 # homeassistant.components.lastfm -pylast==4.0.0 +pylast==4.1.0 # homeassistant.components.forked_daapd pylibrespot-java==0.1.0 @@ -755,7 +778,7 @@ pylibrespot-java==0.1.0 pylitejet==0.1 # homeassistant.components.lutron_caseta -pylutron-caseta==0.7.2 +pylutron-caseta==0.9.0 # homeassistant.components.mailgun pymailgunner==1.4 @@ -808,7 +831,7 @@ pyotgw==1.0b1 pyotp==2.3.0 # homeassistant.components.openweathermap -pyowm==3.1.0 +pyowm==3.1.1 # homeassistant.components.onewire pyownet==0.10.0.post1 @@ -846,7 +869,7 @@ pysignalclirestapi==0.3.4 pysma==0.3.5 # homeassistant.components.smappee -pysmappee==0.2.13 +pysmappee==0.2.16 # homeassistant.components.smartthings pysmartapp==0.3.3 @@ -876,7 +899,7 @@ python-ecobee-api==0.2.8 python-forecastio==1.4.0 # homeassistant.components.izone -python-izone==1.1.2 +python-izone==1.1.3 # homeassistant.components.juicenet python-juicenet==1.0.1 @@ -894,19 +917,19 @@ python-openzwave-mqtt[mqtt-client]==1.4.0 python-songpal==0.12 # homeassistant.components.tado -python-tado==0.8.1 +python-tado==0.10.0 # homeassistant.components.twitch python-twitch-client==0.6.0 # homeassistant.components.velbus -python-velbus==2.1.1 +python-velbus==2.1.2 # homeassistant.components.awair python_awair==0.2.1 # homeassistant.components.tile -pytile==4.0.0 +pytile==5.1.0 # homeassistant.components.traccar pytraccar==0.9.0 @@ -930,10 +953,10 @@ pyvolumio==0.1.3 pywebpush==1.9.2 # homeassistant.components.wemo -pywemo==0.5.6 +pywemo==0.6.1 # homeassistant.components.wilight -pywilight==0.0.65 +pywilight==0.0.66 # homeassistant.components.zerproc pyzerproc==0.4.7 @@ -960,7 +983,7 @@ rokuecp==0.6.0 roombapy==1.6.2 # homeassistant.components.roon -roonapi==0.0.28 +roonapi==0.0.31 # homeassistant.components.rpi_power rpi-bad-power==0.1.0 @@ -974,12 +997,15 @@ samsungctl[websocket]==0.7.1 # homeassistant.components.samsungtv samsungtvws==1.4.0 +# homeassistant.components.dhcp +scapy==2.4.4 + # homeassistant.components.emulated_kasa # homeassistant.components.sense sense_energy==0.8.1 # homeassistant.components.sentry -sentry-sdk==0.19.4 +sentry-sdk==0.19.5 # homeassistant.components.sharkiq sharkiqpy==0.1.8 @@ -988,7 +1014,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==9.6.2 +simplisafe-python==9.6.4 # homeassistant.components.slack slackclient==2.5.0 @@ -1011,6 +1037,9 @@ solaredge==0.0.2 # homeassistant.components.honeywell somecomfort==0.5.2 +# homeassistant.components.somfy_mylink +somfy-mylink-synergy==1.0.6 + # homeassistant.components.sonarr sonarr==0.3.0 @@ -1028,13 +1057,13 @@ spotipy==2.16.1 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.20 +sqlalchemy==1.3.22 # homeassistant.components.srp_energy srpenergy==1.3.2 # homeassistant.components.starline -starline==0.1.3 +starline==0.1.5 # homeassistant.components.statsd statsd==3.2.1 @@ -1049,7 +1078,7 @@ stringcase==1.2.0 sunwatcher==0.2.1 # homeassistant.components.surepetcare -surepy==0.2.6 +surepy==0.4.0 # homeassistant.components.synology_dsm synologydsm-api==1.0.1 @@ -1085,7 +1114,7 @@ twilio==6.32.0 twinkly-client==0.0.2 # homeassistant.components.upb -upb_lib==0.4.11 +upb_lib==0.4.12 # homeassistant.components.upcloud upcloud-api==0.4.5 @@ -1101,7 +1130,7 @@ uvcclient==0.11.0 vilfo-api-client==0.3.2 # homeassistant.components.verisure -vsure==1.5.4 +vsure==1.6.1 # homeassistant.components.vultr vultr==0.1.2 @@ -1144,7 +1173,7 @@ zeep[async]==4.0.0 zeroconf==0.28.8 # homeassistant.components.zha -zha-quirks==0.0.51 +zha-quirks==0.0.53 # homeassistant.components.zha zigpy-cc==0.5.2 @@ -1162,4 +1191,7 @@ zigpy-zigate==0.7.3 zigpy-znp==0.3.0 # homeassistant.components.zha -zigpy==0.30.0 +zigpy==0.32.0 + +# homeassistant.components.zwave_js +zwave-js-server-python==0.17.0 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index e479f5e9ac1..953c7d75394 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -2,7 +2,7 @@ bandit==1.7.0 black==20.8b1 -codespell==1.17.1 +codespell==2.0.0 flake8-docstrings==1.5.0 flake8==3.8.4 isort==5.5.3 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 130fd2cc245..dc1ef9a471b 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -68,6 +68,10 @@ urllib3>=1.24.3 # Constrain H11 to ensure we get a new enough version to support non-rfc line endings h11>=0.12.0 +# Constrain httpcore to fix exception when connection dropped +# https://github.com/encode/httpcore/issues/239 +httpcore>=0.12.3 + # Constrain httplib2 to protect against CVE-2020-11078 httplib2>=0.18.0 diff --git a/script/hassfest/__main__.py b/script/hassfest/__main__.py index 4b2e91524e2..6901622bfb3 100644 --- a/script/hassfest/__main__.py +++ b/script/hassfest/__main__.py @@ -9,6 +9,7 @@ from . import ( config_flow, coverage, dependencies, + dhcp, json, manifest, mqtt, @@ -31,6 +32,7 @@ INTEGRATION_PLUGINS = [ ssdp, translations, zeroconf, + dhcp, ] HASS_PLUGINS = [ coverage, @@ -181,7 +183,7 @@ def print_integrations_status(config, integrations, *, show_fixable_errors=True) print(f"Integration {integration.domain}{extra}:") for error in integration.errors: if show_fixable_errors or not error.fixable: - print("*", error) + print("*", "[ERROR]", error) for warning in integration.warnings: print("*", "[WARNING]", warning) print() diff --git a/script/hassfest/config_flow.py b/script/hassfest/config_flow.py index d3402c3dc9a..9ae0daee1b0 100644 --- a/script/hassfest/config_flow.py +++ b/script/hassfest/config_flow.py @@ -48,6 +48,11 @@ def validate_integration(config: Config, integration: Integration): "config_flow", "Zeroconf information in a manifest requires a config flow to exist", ) + if integration.manifest.get("dhcp"): + integration.add_error( + "config_flow", + "DHCP information in a manifest requires a config flow to exist", + ) return config_flow = config_flow_file.read_text() @@ -59,6 +64,7 @@ def validate_integration(config: Config, integration: Integration): or "async_step_mqtt" in config_flow or "async_step_ssdp" in config_flow or "async_step_zeroconf" in config_flow + or "async_step_dhcp" in config_flow ) if not needs_unique_id: @@ -100,6 +106,7 @@ def generate_and_validate(integrations: Dict[str, Integration], config: Config): or integration.manifest.get("mqtt") or integration.manifest.get("ssdp") or integration.manifest.get("zeroconf") + or integration.manifest.get("dhcp") ): continue diff --git a/script/hassfest/dhcp.py b/script/hassfest/dhcp.py new file mode 100644 index 00000000000..fbf695a9f73 --- /dev/null +++ b/script/hassfest/dhcp.py @@ -0,0 +1,63 @@ +"""Generate dhcp file.""" +import json +from typing import Dict, List + +from .model import Config, Integration + +BASE = """ +\"\"\"Automatically generated by hassfest. + +To update, run python3 -m script.hassfest +\"\"\" + +# fmt: off + +DHCP = {} +""".strip() + + +def generate_and_validate(integrations: List[Dict[str, str]]): + """Validate and generate dhcp data.""" + match_list = [] + + for domain in sorted(integrations): + integration = integrations[domain] + + if not integration.manifest: + continue + + match_types = integration.manifest.get("dhcp", []) + + if not match_types: + continue + + for entry in match_types: + match_list.append({"domain": domain, **entry}) + + return BASE.format(json.dumps(match_list, indent=4)) + + +def validate(integrations: Dict[str, Integration], config: Config): + """Validate dhcp file.""" + dhcp_path = config.root / "homeassistant/generated/dhcp.py" + config.cache["dhcp"] = content = generate_and_validate(integrations) + + if config.specific_integrations: + return + + with open(str(dhcp_path)) as fp: + current = fp.read().strip() + if current != content: + config.add_error( + "dhcp", + "File dhcp.py is not up to date. Run python3 -m script.hassfest", + fixable=True, + ) + return + + +def generate(integrations: Dict[str, Integration], config: Config): + """Generate dhcp file.""" + dhcp_path = config.root / "homeassistant/generated/dhcp.py" + with open(str(dhcp_path), "w") as fp: + fp.write(f"{config.cache['dhcp']}\n") diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 7500483ec53..3beb6aadfc5 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -2,6 +2,8 @@ from typing import Dict from urllib.parse import urlparse +from awesomeversion import AwesomeVersion +from awesomeversion.strategy import AwesomeVersionStrategy import voluptuous as vol from voluptuous.humanize import humanize_error @@ -49,6 +51,22 @@ def verify_uppercase(value: str): return value +def verify_version(value: str): + """Verify the version.""" + version = AwesomeVersion(value) + if version.strategy not in [ + AwesomeVersionStrategy.CALVER, + AwesomeVersionStrategy.SEMVER, + AwesomeVersionStrategy.SIMPLEVER, + AwesomeVersionStrategy.BUILDVER, + AwesomeVersionStrategy.PEP440, + ]: + raise vol.Invalid( + f"'{version}' is not a valid version. This will cause a future version of Home Assistant to block this integration.", + ) + return value + + MANIFEST_SCHEMA = vol.Schema( { vol.Required("domain"): str, @@ -71,6 +89,14 @@ MANIFEST_SCHEMA = vol.Schema( vol.All([vol.All(vol.Schema({}, extra=vol.ALLOW_EXTRA), vol.Length(min=1))]) ), vol.Optional("homekit"): vol.Schema({vol.Optional("models"): [str]}), + vol.Optional("dhcp"): [ + vol.Schema( + { + vol.Optional("macaddress"): vol.All(str, verify_uppercase), + vol.Optional("hostname"): vol.All(str, verify_lowercase), + } + ) + ], vol.Required("documentation"): vol.All( vol.Url(), documentation_url # pylint: disable=no-value-for-parameter ), @@ -86,21 +112,45 @@ MANIFEST_SCHEMA = vol.Schema( } ) +CUSTOM_INTEGRATION_MANIFEST_SCHEMA = MANIFEST_SCHEMA.extend( + { + vol.Optional("version"): vol.All(str, verify_version), + } +) + + +def validate_version(integration: Integration): + """ + Validate the version of the integration. + + Will be removed when the version key is no longer optional for custom integrations. + """ + if not integration.manifest.get("version"): + integration.add_warning( + "manifest", + "No 'version' key in the manifest file. This will cause a future version of Home Assistant to block this integration.", + ) + return + def validate_manifest(integration: Integration): """Validate manifest.""" try: - MANIFEST_SCHEMA(integration.manifest) + if integration.core: + MANIFEST_SCHEMA(integration.manifest) + else: + CUSTOM_INTEGRATION_MANIFEST_SCHEMA(integration.manifest) except vol.Invalid as err: integration.add_error( "manifest", f"Invalid manifest: {humanize_error(integration.manifest, err)}" ) - integration.manifest = None - return if integration.manifest["domain"] != integration.path.name: integration.add_error("manifest", "Domain does not match dir name") + if not integration.core: + validate_version(integration) + def validate(integrations: Dict[str, Integration], config): """Handle all integrations manifests.""" diff --git a/script/hassfest/model.py b/script/hassfest/model.py index 8c55c2818f1..0750ef10b6c 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -74,6 +74,11 @@ class Integration: """Integration domain.""" return self.path.name + @property + def core(self) -> bool: + """Core integration.""" + return self.path.as_posix().startswith("homeassistant/components") + @property def disabled(self) -> Optional[str]: """List of disabled.""" diff --git a/script/scaffold/docs.py b/script/scaffold/docs.py index f4416a7b7e8..3a871301b97 100644 --- a/script/scaffold/docs.py +++ b/script/scaffold/docs.py @@ -35,6 +35,11 @@ DATA = { "docs": "https://developers.home-assistant.io/docs/en/reproduce_state_index.html", "extra": "You will now need to update the code to make sure that every attribute that can occur in the state will cause the right service to be called.", }, + "significant_change": { + "title": "Significant Change", + "docs": "https://developers.home-assistant.io/docs/en/significant_change_index.html", + "extra": "You will now need to update the code to make sure that entities with different device classes are correctly considered.", + }, } @@ -73,4 +78,5 @@ def print_relevant_docs(template: str, info: Info) -> None: ) if "extra" in data: + print() print(data["extra"]) diff --git a/script/scaffold/templates/config_flow/integration/__init__.py b/script/scaffold/templates/config_flow/integration/__init__.py index 4a206981c3c..c6df6e99979 100644 --- a/script/scaffold/templates/config_flow/integration/__init__.py +++ b/script/scaffold/templates/config_flow/integration/__init__.py @@ -1,15 +1,11 @@ """The NEW_NAME integration.""" import asyncio -import voluptuous as vol - from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from .const import DOMAIN -CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) - # TODO List the platforms that you want to support. # For your initial PR, limit it to 1 platform. PLATFORMS = ["light"] diff --git a/script/scaffold/templates/config_flow/tests/test_config_flow.py b/script/scaffold/templates/config_flow/tests/test_config_flow.py index a69bea2abd8..04eab6e683c 100644 --- a/script/scaffold/templates/config_flow/tests/test_config_flow.py +++ b/script/scaffold/templates/config_flow/tests/test_config_flow.py @@ -1,10 +1,10 @@ """Test the NEW_NAME config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, setup from homeassistant.components.NEW_DOMAIN.config_flow import CannotConnect, InvalidAuth from homeassistant.components.NEW_DOMAIN.const import DOMAIN -from tests.async_mock import patch - async def test_form(hass): """Test we get the form.""" diff --git a/script/scaffold/templates/config_flow_discovery/integration/__init__.py b/script/scaffold/templates/config_flow_discovery/integration/__init__.py index 4a206981c3c..c6df6e99979 100644 --- a/script/scaffold/templates/config_flow_discovery/integration/__init__.py +++ b/script/scaffold/templates/config_flow_discovery/integration/__init__.py @@ -1,15 +1,11 @@ """The NEW_NAME integration.""" import asyncio -import voluptuous as vol - from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from .const import DOMAIN -CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) - # TODO List the platforms that you want to support. # For your initial PR, limit it to 1 platform. PLATFORMS = ["light"] diff --git a/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py b/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py index ed974601646..dd0fc3446b3 100644 --- a/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py +++ b/script/scaffold/templates/config_flow_oauth2/tests/test_config_flow.py @@ -1,4 +1,6 @@ """Test the NEW_NAME config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, setup from homeassistant.components.NEW_DOMAIN.const import ( DOMAIN, @@ -7,8 +9,6 @@ from homeassistant.components.NEW_DOMAIN.const import ( ) from homeassistant.helpers import config_entry_oauth2_flow -from tests.async_mock import patch - CLIENT_ID = "1234" CLIENT_SECRET = "5678" diff --git a/script/scaffold/templates/significant_change/integration/significant_change.py b/script/scaffold/templates/significant_change/integration/significant_change.py new file mode 100644 index 00000000000..23a00c603ac --- /dev/null +++ b/script/scaffold/templates/significant_change/integration/significant_change.py @@ -0,0 +1,23 @@ +"""Helper to test significant NEW_NAME state changes.""" +from typing import Any, Optional + +from homeassistant.const import ATTR_DEVICE_CLASS +from homeassistant.core import HomeAssistant, callback + + +@callback +def async_check_significant_change( + hass: HomeAssistant, + old_state: str, + old_attrs: dict, + new_state: str, + new_attrs: dict, + **kwargs: Any, +) -> Optional[bool]: + """Test if state significantly changed.""" + device_class = new_attrs.get(ATTR_DEVICE_CLASS) + + if device_class is None: + return None + + return None diff --git a/script/scaffold/templates/significant_change/tests/test_significant_change.py b/script/scaffold/templates/significant_change/tests/test_significant_change.py new file mode 100644 index 00000000000..ac339071748 --- /dev/null +++ b/script/scaffold/templates/significant_change/tests/test_significant_change.py @@ -0,0 +1,11 @@ +"""Test the NEW_NAME significant change platform.""" +from homeassistant.components.NEW_DOMAIN.significant_change import ( + async_check_significant_change, +) + + +async def test_significant_change(): + """Detect NEW_NAME significant changes.""" + attrs = {} + assert not async_check_significant_change(None, "on", attrs, "on", attrs) + assert async_check_significant_change(None, "on", attrs, "off", attrs) diff --git a/script/test b/script/test deleted file mode 100755 index 8c4688a4d65..00000000000 --- a/script/test +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -# Executes the tests with tox. - -cd "$(dirname "$0")/.." - -tox -e py36 diff --git a/setup.cfg b/setup.cfg index de5092dcecf..7761ff2d67e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -11,7 +11,8 @@ classifier = Intended Audience :: Developers License :: OSI Approved :: Apache Software License Operating System :: OS Independent - Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 Topic :: Home Automation [flake8] @@ -31,7 +32,7 @@ ignore = W504 [mypy] -python_version = 3.7 +python_version = 3.8 show_error_codes = true ignore_errors = true follow_imports = silent @@ -41,7 +42,7 @@ warn_redundant_casts = true warn_unused_configs = true -[mypy-homeassistant.block_async_io,homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.__init__,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.runner,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.huawei_lte.*,homeassistant.components.hyperion.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.number.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zone.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*,tests.components.hyperion.*] +[mypy-homeassistant.block_async_io,homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.__init__,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.runner,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.huawei_lte.*,homeassistant.components.hyperion.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.number.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.slack.*,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zone.*,homeassistant.components.zwave_js.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*,tests.components.hyperion.*] strict = true ignore_errors = false warn_unreachable = true diff --git a/setup.py b/setup.py index 096e376b1a1..7f77e3795b4 100755 --- a/setup.py +++ b/setup.py @@ -37,19 +37,18 @@ REQUIRES = [ "async_timeout==3.0.1", "attrs==19.3.0", "bcrypt==3.1.7", - "certifi>=2020.6.20", + "certifi>=2020.12.5", "ciso8601==2.1.3", "httpx==0.16.1", - "importlib-metadata==1.6.0;python_version<'3.8'", "jinja2>=2.11.2", "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. "cryptography==3.2", "pip>=8.0.3,<20.3", "python-slugify==4.0.1", - "pytz>=2020.1", - "pyyaml==5.3.1", - "requests==2.25.0", + "pytz>=2020.5", + "pyyaml==5.4.1", + "requests==2.25.1", "ruamel.yaml==0.15.100", "voluptuous==0.12.1", "voluptuous-serialize==2.4.0", diff --git a/tests/async_mock.py b/tests/async_mock.py deleted file mode 100644 index 8257ddd3b3b..00000000000 --- a/tests/async_mock.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Mock utilities that are async aware.""" -import sys - -if sys.version_info[:2] < (3, 8): - from asynctest.mock import * # noqa - - AsyncMock = CoroutineMock # noqa: F405 -else: - from unittest.mock import * # noqa diff --git a/tests/auth/mfa_modules/test_notify.py b/tests/auth/mfa_modules/test_notify.py index 65ee5d5d0c5..c79d76baf4f 100644 --- a/tests/auth/mfa_modules/test_notify.py +++ b/tests/auth/mfa_modules/test_notify.py @@ -1,12 +1,12 @@ """Test the HMAC-based One Time Password (MFA) auth module.""" import asyncio +from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.auth import auth_manager_from_config, models as auth_models from homeassistant.auth.mfa_modules import auth_mfa_module_from_config from homeassistant.components.notify import NOTIFY_SERVICE_SCHEMA -from tests.async_mock import patch from tests.common import MockUser, async_mock_service MOCK_CODE = "123456" diff --git a/tests/auth/mfa_modules/test_totp.py b/tests/auth/mfa_modules/test_totp.py index b14b20297eb..d0a4f3cf3ac 100644 --- a/tests/auth/mfa_modules/test_totp.py +++ b/tests/auth/mfa_modules/test_totp.py @@ -1,11 +1,11 @@ """Test the Time-based One Time Password (MFA) auth module.""" import asyncio +from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.auth import auth_manager_from_config, models as auth_models from homeassistant.auth.mfa_modules import auth_mfa_module_from_config -from tests.async_mock import patch from tests.common import MockUser MOCK_CODE = "123456" diff --git a/tests/auth/providers/test_command_line.py b/tests/auth/providers/test_command_line.py index 3915950cedb..e437ca9e331 100644 --- a/tests/auth/providers/test_command_line.py +++ b/tests/auth/providers/test_command_line.py @@ -1,6 +1,7 @@ """Tests for the command_line auth provider.""" import os +from unittest.mock import AsyncMock import uuid import pytest @@ -10,8 +11,6 @@ from homeassistant.auth import AuthManager, auth_store, models as auth_models from homeassistant.auth.providers import command_line from homeassistant.const import CONF_TYPE -from tests.async_mock import AsyncMock - @pytest.fixture def store(hass): diff --git a/tests/auth/providers/test_homeassistant.py b/tests/auth/providers/test_homeassistant.py index c804b237e8b..62093df7210 100644 --- a/tests/auth/providers/test_homeassistant.py +++ b/tests/auth/providers/test_homeassistant.py @@ -1,5 +1,6 @@ """Test the Home Assistant local auth provider.""" import asyncio +from unittest.mock import Mock, patch import pytest import voluptuous as vol @@ -11,8 +12,6 @@ from homeassistant.auth.providers import ( homeassistant as hass_auth, ) -from tests.async_mock import Mock, patch - @pytest.fixture def data(hass): diff --git a/tests/auth/providers/test_insecure_example.py b/tests/auth/providers/test_insecure_example.py index c2b16cbafab..235f9a4735f 100644 --- a/tests/auth/providers/test_insecure_example.py +++ b/tests/auth/providers/test_insecure_example.py @@ -1,4 +1,5 @@ """Tests for the insecure example auth provider.""" +from unittest.mock import AsyncMock import uuid import pytest @@ -6,8 +7,6 @@ import pytest from homeassistant.auth import AuthManager, auth_store, models as auth_models from homeassistant.auth.providers import insecure_example -from tests.async_mock import AsyncMock - @pytest.fixture def store(hass): diff --git a/tests/auth/test_auth_store.py b/tests/auth/test_auth_store.py index 78ab9829ab6..4ab0fc4a360 100644 --- a/tests/auth/test_auth_store.py +++ b/tests/auth/test_auth_store.py @@ -1,10 +1,9 @@ """Tests for the auth store.""" import asyncio +from unittest.mock import patch from homeassistant.auth import auth_store -from tests.async_mock import patch - async def test_loading_no_group_data_format(hass, hass_storage): """Test we correctly load old data without any groups.""" diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index f303a59179b..edcd01d51e1 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -1,5 +1,6 @@ """Tests for the Home Assistant auth module.""" from datetime import timedelta +from unittest.mock import Mock, patch import jwt import pytest @@ -11,7 +12,6 @@ from homeassistant.auth.const import MFA_SESSION_EXPIRATION from homeassistant.core import callback from homeassistant.util import dt as dt_util -from tests.async_mock import Mock, patch from tests.common import CLIENT_ID, MockUser, ensure_auth_manager_loaded, flush_store diff --git a/tests/common.py b/tests/common.py index ce07f5ab615..2621f2f4b15 100644 --- a/tests/common.py +++ b/tests/common.py @@ -12,6 +12,7 @@ import os import pathlib import threading import time +from unittest.mock import AsyncMock, Mock, patch import uuid from aiohttp.test_utils import unused_port as get_test_instance_port # noqa @@ -60,8 +61,6 @@ import homeassistant.util.dt as date_util from homeassistant.util.unit_system import METRIC_SYSTEM import homeassistant.util.yaml.loader as yaml_loader -from tests.async_mock import AsyncMock, Mock, patch - _LOGGER = logging.getLogger(__name__) INSTANCES = [] CLIENT_ID = "https://example.com/app" diff --git a/tests/components/abode/common.py b/tests/components/abode/common.py index 157a5441bb1..aabc732daa2 100644 --- a/tests/components/abode/common.py +++ b/tests/components/abode/common.py @@ -1,9 +1,10 @@ """Common methods used across tests for Abode.""" +from unittest.mock import patch + from homeassistant.components.abode import DOMAIN as ABODE_DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/abode/test_alarm_control_panel.py b/tests/components/abode/test_alarm_control_panel.py index b45599c408e..63ae20441f5 100644 --- a/tests/components/abode/test_alarm_control_panel.py +++ b/tests/components/abode/test_alarm_control_panel.py @@ -1,4 +1,6 @@ """Tests for the Abode alarm control panel device.""" +from unittest.mock import PropertyMock, patch + import abodepy.helpers.constants as CONST from homeassistant.components.abode import ATTR_DEVICE_ID @@ -17,8 +19,6 @@ from homeassistant.const import ( from .common import setup_platform -from tests.async_mock import PropertyMock, patch - DEVICE_ID = "alarm_control_panel.abode_alarm" diff --git a/tests/components/abode/test_camera.py b/tests/components/abode/test_camera.py index 9db03d90222..06540955464 100644 --- a/tests/components/abode/test_camera.py +++ b/tests/components/abode/test_camera.py @@ -1,12 +1,12 @@ """Tests for the Abode camera device.""" +from unittest.mock import patch + from homeassistant.components.abode.const import DOMAIN as ABODE_DOMAIN from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.const import ATTR_ENTITY_ID, STATE_IDLE from .common import setup_platform -from tests.async_mock import patch - async def test_entity_registry(hass): """Tests that the devices are registered in the entity registry.""" diff --git a/tests/components/abode/test_config_flow.py b/tests/components/abode/test_config_flow.py index f1445db340f..026735ed536 100644 --- a/tests/components/abode/test_config_flow.py +++ b/tests/components/abode/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for the Abode config flow.""" +from unittest.mock import patch + from abodepy.exceptions import AbodeAuthenticationException from abodepy.helpers.errors import MFA_CODE_REQUIRED @@ -13,7 +15,6 @@ from homeassistant.const import ( HTTP_INTERNAL_SERVER_ERROR, ) -from tests.async_mock import patch from tests.common import MockConfigEntry CONF_POLLING = "polling" diff --git a/tests/components/abode/test_cover.py b/tests/components/abode/test_cover.py index b166ec5464a..bb1b8fceffb 100644 --- a/tests/components/abode/test_cover.py +++ b/tests/components/abode/test_cover.py @@ -1,4 +1,6 @@ """Tests for the Abode cover device.""" +from unittest.mock import patch + from homeassistant.components.abode import ATTR_DEVICE_ID from homeassistant.components.cover import DOMAIN as COVER_DOMAIN from homeassistant.const import ( @@ -11,8 +13,6 @@ from homeassistant.const import ( from .common import setup_platform -from tests.async_mock import patch - DEVICE_ID = "cover.garage_door" diff --git a/tests/components/abode/test_init.py b/tests/components/abode/test_init.py index 68f7ce9dd03..b4f3dbd736b 100644 --- a/tests/components/abode/test_init.py +++ b/tests/components/abode/test_init.py @@ -1,4 +1,6 @@ """Tests for the Abode module.""" +from unittest.mock import patch + from abodepy.exceptions import AbodeAuthenticationException from homeassistant.components.abode import ( @@ -12,8 +14,6 @@ from homeassistant.const import CONF_USERNAME, HTTP_BAD_REQUEST from .common import setup_platform -from tests.async_mock import patch - async def test_change_settings(hass): """Test change_setting service.""" diff --git a/tests/components/abode/test_light.py b/tests/components/abode/test_light.py index 6506746783c..f0eee4b209b 100644 --- a/tests/components/abode/test_light.py +++ b/tests/components/abode/test_light.py @@ -1,4 +1,6 @@ """Tests for the Abode light device.""" +from unittest.mock import patch + from homeassistant.components.abode import ATTR_DEVICE_ID from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -17,8 +19,6 @@ from homeassistant.const import ( from .common import setup_platform -from tests.async_mock import patch - DEVICE_ID = "light.living_room_lamp" diff --git a/tests/components/abode/test_lock.py b/tests/components/abode/test_lock.py index 6850eebe0ce..45e17861d33 100644 --- a/tests/components/abode/test_lock.py +++ b/tests/components/abode/test_lock.py @@ -1,4 +1,6 @@ """Tests for the Abode lock device.""" +from unittest.mock import patch + from homeassistant.components.abode import ATTR_DEVICE_ID from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.const import ( @@ -11,8 +13,6 @@ from homeassistant.const import ( from .common import setup_platform -from tests.async_mock import patch - DEVICE_ID = "lock.test_lock" diff --git a/tests/components/abode/test_switch.py b/tests/components/abode/test_switch.py index 5c480b33225..3ec9648d87d 100644 --- a/tests/components/abode/test_switch.py +++ b/tests/components/abode/test_switch.py @@ -1,4 +1,6 @@ """Tests for the Abode switch device.""" +from unittest.mock import patch + from homeassistant.components.abode import ( DOMAIN as ABODE_DOMAIN, SERVICE_TRIGGER_AUTOMATION, @@ -14,8 +16,6 @@ from homeassistant.const import ( from .common import setup_platform -from tests.async_mock import patch - AUTOMATION_ID = "switch.test_automation" AUTOMATION_UID = "47fae27488f74f55b964a81a066c3a01" DEVICE_ID = "switch.test_switch" diff --git a/tests/components/accuweather/__init__.py b/tests/components/accuweather/__init__.py index 28e53d1e2fc..d78eac4269b 100644 --- a/tests/components/accuweather/__init__.py +++ b/tests/components/accuweather/__init__.py @@ -1,9 +1,9 @@ """Tests for AccuWeather.""" import json +from unittest.mock import patch from homeassistant.components.accuweather.const import DOMAIN -from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture diff --git a/tests/components/accuweather/test_config_flow.py b/tests/components/accuweather/test_config_flow.py index 89159d7c1bf..1d9feecda3c 100644 --- a/tests/components/accuweather/test_config_flow.py +++ b/tests/components/accuweather/test_config_flow.py @@ -1,5 +1,6 @@ """Define tests for the AccuWeather config flow.""" import json +from unittest.mock import patch from accuweather import ApiError, InvalidApiKeyError, RequestsExceededError @@ -8,7 +9,6 @@ from homeassistant.components.accuweather.const import CONF_FORECAST, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture VALID_CONFIG = { diff --git a/tests/components/accuweather/test_init.py b/tests/components/accuweather/test_init.py index 0a54132fd68..bb45e894e74 100644 --- a/tests/components/accuweather/test_init.py +++ b/tests/components/accuweather/test_init.py @@ -1,4 +1,10 @@ """Test init of AccuWeather integration.""" +from datetime import timedelta +import json +from unittest.mock import patch + +from accuweather import ApiError + from homeassistant.components.accuweather.const import DOMAIN from homeassistant.config_entries import ( ENTRY_STATE_LOADED, @@ -6,9 +12,9 @@ from homeassistant.config_entries import ( ENTRY_STATE_SETUP_RETRY, ) from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.util.dt import utcnow -from tests.async_mock import patch -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture from tests.components.accuweather import init_integration @@ -38,7 +44,7 @@ async def test_config_not_ready(hass): with patch( "homeassistant.components.accuweather.AccuWeather._async_get_data", - side_effect=ConnectionError(), + side_effect=ApiError("API Error"), ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) @@ -57,3 +63,53 @@ async def test_unload_entry(hass): assert entry.state == ENTRY_STATE_NOT_LOADED assert not hass.data.get(DOMAIN) + + +async def test_update_interval(hass): + """Test correct update interval.""" + entry = await init_integration(hass) + + assert entry.state == ENTRY_STATE_LOADED + + current = json.loads(load_fixture("accuweather/current_conditions_data.json")) + future = utcnow() + timedelta(minutes=40) + + with patch( + "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", + return_value=current, + ) as mock_current: + + assert mock_current.call_count == 0 + + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + assert mock_current.call_count == 1 + + +async def test_update_interval_forecast(hass): + """Test correct update interval when forecast is True.""" + entry = await init_integration(hass, forecast=True) + + assert entry.state == ENTRY_STATE_LOADED + + current = json.loads(load_fixture("accuweather/current_conditions_data.json")) + forecast = json.loads(load_fixture("accuweather/forecast_data.json")) + future = utcnow() + timedelta(minutes=80) + + with patch( + "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", + return_value=current, + ) as mock_current, patch( + "homeassistant.components.accuweather.AccuWeather.async_get_forecast", + return_value=forecast, + ) as mock_forecast: + + assert mock_current.call_count == 0 + assert mock_forecast.call_count == 0 + + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + assert mock_current.call_count == 1 + assert mock_forecast.call_count == 1 diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py index 185f6024886..361422883d4 100644 --- a/tests/components/accuweather/test_sensor.py +++ b/tests/components/accuweather/test_sensor.py @@ -1,6 +1,7 @@ """Test sensor of AccuWeather integration.""" from datetime import timedelta import json +from unittest.mock import patch from homeassistant.components.accuweather.const import ATTRIBUTION, DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -24,7 +25,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from tests.async_mock import patch from tests.common import async_fire_time_changed, load_fixture from tests.components.accuweather import init_integration diff --git a/tests/components/accuweather/test_system_health.py b/tests/components/accuweather/test_system_health.py index cf8c931e123..749f516e44c 100644 --- a/tests/components/accuweather/test_system_health.py +++ b/tests/components/accuweather/test_system_health.py @@ -1,12 +1,12 @@ """Test AccuWeather system health.""" import asyncio +from unittest.mock import Mock from aiohttp import ClientError from homeassistant.components.accuweather.const import COORDINATOR, DOMAIN from homeassistant.setup import async_setup_component -from tests.async_mock import Mock from tests.common import get_system_health_info diff --git a/tests/components/accuweather/test_weather.py b/tests/components/accuweather/test_weather.py index 692b2ae243f..0c1559ef0d6 100644 --- a/tests/components/accuweather/test_weather.py +++ b/tests/components/accuweather/test_weather.py @@ -1,6 +1,7 @@ """Test weather of AccuWeather integration.""" from datetime import timedelta import json +from unittest.mock import patch from homeassistant.components.accuweather.const import ATTRIBUTION from homeassistant.components.weather import ( @@ -25,7 +26,6 @@ from homeassistant.const import ATTR_ATTRIBUTION, ATTR_ENTITY_ID, STATE_UNAVAILA from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from tests.async_mock import patch from tests.common import async_fire_time_changed, load_fixture from tests.components.accuweather import init_integration diff --git a/tests/components/acmeda/test_config_flow.py b/tests/components/acmeda/test_config_flow.py index 2aacedc3680..269a72cd839 100644 --- a/tests/components/acmeda/test_config_flow.py +++ b/tests/components/acmeda/test_config_flow.py @@ -1,4 +1,6 @@ """Define tests for the Acmeda config flow.""" +from unittest.mock import patch + import aiopulse import pytest @@ -7,7 +9,6 @@ from homeassistant.components.acmeda.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST -from tests.async_mock import patch from tests.common import MockConfigEntry DUMMY_HOST1 = "127.0.0.1" diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index 36263335dac..06fe235741f 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -1,5 +1,7 @@ """Tests for the AdGuard Home config flow.""" +from unittest.mock import patch + import aiohttp from homeassistant import config_entries, data_entry_flow @@ -15,7 +17,6 @@ from homeassistant.const import ( CONTENT_TYPE_JSON, ) -from tests.async_mock import patch from tests.common import MockConfigEntry FIXTURE_USER_INPUT = { diff --git a/tests/components/advantage_air/test_config_flow.py b/tests/components/advantage_air/test_config_flow.py index 248ee25858b..a8e219fff89 100644 --- a/tests/components/advantage_air/test_config_flow.py +++ b/tests/components/advantage_air/test_config_flow.py @@ -1,9 +1,10 @@ """Test the Advantage Air config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, data_entry_flow from homeassistant.components.advantage_air.const import DOMAIN -from tests.async_mock import patch from tests.components.advantage_air import TEST_SYSTEM_DATA, TEST_SYSTEM_URL, USER_INPUT diff --git a/tests/components/airly/__init__.py b/tests/components/airly/__init__.py index 197864b807c..64f2059857a 100644 --- a/tests/components/airly/__init__.py +++ b/tests/components/airly/__init__.py @@ -3,6 +3,7 @@ from homeassistant.components.airly.const import DOMAIN from tests.common import MockConfigEntry, load_fixture +API_NEAREST_URL = "https://airapi.airly.eu/v2/measurements/nearest?lat=123.000000&lng=456.000000&maxDistanceKM=5.000000" API_POINT_URL = ( "https://airapi.airly.eu/v2/measurements/point?lat=123.000000&lng=456.000000" ) diff --git a/tests/components/airly/test_config_flow.py b/tests/components/airly/test_config_flow.py index 46dc5510b18..5683a06bb28 100644 --- a/tests/components/airly/test_config_flow.py +++ b/tests/components/airly/test_config_flow.py @@ -2,17 +2,18 @@ from airly.exceptions import AirlyError from homeassistant import data_entry_flow -from homeassistant.components.airly.const import DOMAIN +from homeassistant.components.airly.const import CONF_USE_NEAREST, DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME, + HTTP_NOT_FOUND, HTTP_UNAUTHORIZED, ) -from . import API_POINT_URL +from . import API_NEAREST_URL, API_POINT_URL from tests.common import MockConfigEntry, load_fixture, patch @@ -54,6 +55,11 @@ async def test_invalid_location(hass, aioclient_mock): """Test that errors are shown when location is invalid.""" aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_no_station.json")) + aioclient_mock.get( + API_NEAREST_URL, + exc=AirlyError(HTTP_NOT_FOUND, {"message": "Installation was not found"}), + ) + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=CONFIG ) @@ -88,3 +94,24 @@ async def test_create_entry(hass, aioclient_mock): assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] assert result["data"][CONF_API_KEY] == CONFIG[CONF_API_KEY] + assert result["data"][CONF_USE_NEAREST] is False + + +async def test_create_entry_with_nearest_method(hass, aioclient_mock): + """Test that the user step works with nearest method.""" + + aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_no_station.json")) + + aioclient_mock.get(API_NEAREST_URL, text=load_fixture("airly_valid_station.json")) + + with patch("homeassistant.components.airly.async_setup_entry", return_value=True): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONFIG + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == CONFIG[CONF_NAME] + assert result["data"][CONF_LATITUDE] == CONFIG[CONF_LATITUDE] + assert result["data"][CONF_LONGITUDE] == CONFIG[CONF_LONGITUDE] + assert result["data"][CONF_API_KEY] == CONFIG[CONF_API_KEY] + assert result["data"][CONF_USE_NEAREST] is True diff --git a/tests/components/airly/test_init.py b/tests/components/airly/test_init.py index cb0ccf268f7..2898bd5c6f6 100644 --- a/tests/components/airly/test_init.py +++ b/tests/components/airly/test_init.py @@ -36,6 +36,7 @@ async def test_config_not_ready(hass, aioclient_mock): "latitude": 123, "longitude": 456, "name": "Home", + "use_nearest": True, }, ) diff --git a/tests/components/airly/test_system_health.py b/tests/components/airly/test_system_health.py index b1f8119e880..02ee67ae452 100644 --- a/tests/components/airly/test_system_health.py +++ b/tests/components/airly/test_system_health.py @@ -1,12 +1,12 @@ """Test Airly system health.""" import asyncio +from unittest.mock import Mock from aiohttp import ClientError from homeassistant.components.airly.const import DOMAIN from homeassistant.setup import async_setup_component -from tests.async_mock import Mock from tests.common import get_system_health_info diff --git a/tests/components/airnow/__init__.py b/tests/components/airnow/__init__.py new file mode 100644 index 00000000000..d7fc1922ee8 --- /dev/null +++ b/tests/components/airnow/__init__.py @@ -0,0 +1 @@ +"""Tests for the AirNow integration.""" diff --git a/tests/components/airnow/test_config_flow.py b/tests/components/airnow/test_config_flow.py new file mode 100644 index 00000000000..f7533b7f5ac --- /dev/null +++ b/tests/components/airnow/test_config_flow.py @@ -0,0 +1,186 @@ +"""Test the AirNow config flow.""" +from unittest.mock import patch + +from pyairnow.errors import AirNowError, InvalidKeyError + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.airnow.const import DOMAIN +from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_RADIUS + +from tests.common import MockConfigEntry + +CONFIG = { + CONF_API_KEY: "abc123", + CONF_LATITUDE: 34.053718, + CONF_LONGITUDE: -118.244842, + CONF_RADIUS: 75, +} + +# Mock AirNow Response +MOCK_RESPONSE = [ + { + "DateObserved": "2020-12-20", + "HourObserved": 15, + "LocalTimeZone": "PST", + "ReportingArea": "Central LA CO", + "StateCode": "CA", + "Latitude": 34.0663, + "Longitude": -118.2266, + "ParameterName": "O3", + "AQI": 44, + "Category": { + "Number": 1, + "Name": "Good", + }, + }, + { + "DateObserved": "2020-12-20", + "HourObserved": 15, + "LocalTimeZone": "PST", + "ReportingArea": "Central LA CO", + "StateCode": "CA", + "Latitude": 34.0663, + "Longitude": -118.2266, + "ParameterName": "PM2.5", + "AQI": 37, + "Category": { + "Number": 1, + "Name": "Good", + }, + }, + { + "DateObserved": "2020-12-20", + "HourObserved": 15, + "LocalTimeZone": "PST", + "ReportingArea": "Central LA CO", + "StateCode": "CA", + "Latitude": 34.0663, + "Longitude": -118.2266, + "ParameterName": "PM10", + "AQI": 11, + "Category": { + "Number": 1, + "Name": "Good", + }, + }, +] + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch("pyairnow.WebServiceAPI._get", return_value=MOCK_RESPONSE,), patch( + "homeassistant.components.airnow.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.airnow.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + CONFIG, + ) + + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["data"] == CONFIG + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "pyairnow.WebServiceAPI._get", + side_effect=InvalidKeyError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + CONFIG, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_invalid_location(hass): + """Test we handle invalid location.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("pyairnow.WebServiceAPI._get", return_value={}): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + CONFIG, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_location"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "pyairnow.WebServiceAPI._get", + side_effect=AirNowError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + CONFIG, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_unexpected(hass): + """Test we handle an unexpected error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.airnow.config_flow.validate_input", + side_effect=RuntimeError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + CONFIG, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} + + +async def test_entry_already_exists(hass): + """Test that the form aborts if the Lat/Lng is already configured.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_id = f"{CONFIG[CONF_LATITUDE]}-{CONFIG[CONF_LONGITUDE]}" + mock_entry = MockConfigEntry(domain=DOMAIN, unique_id=mock_id) + mock_entry.add_to_hass(hass) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + CONFIG, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index 3eb22360c5c..4e550d94b09 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -1,4 +1,6 @@ """Define tests for the AirVisual config flow.""" +from unittest.mock import patch + from pyairvisual.errors import InvalidKeyError, NodeProError from homeassistant import data_entry_flow @@ -20,7 +22,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/alarmdecoder/test_config_flow.py b/tests/components/alarmdecoder/test_config_flow.py index b858671fb54..52d1686691c 100644 --- a/tests/components/alarmdecoder/test_config_flow.py +++ b/tests/components/alarmdecoder/test_config_flow.py @@ -1,4 +1,6 @@ """Test the AlarmDecoder config flow.""" +from unittest.mock import patch + from alarmdecoder.util import NoDeviceError import pytest @@ -29,7 +31,6 @@ from homeassistant.components.binary_sensor import DEVICE_CLASS_WINDOW from homeassistant.const import CONF_HOST, CONF_PORT, CONF_PROTOCOL from homeassistant.core import HomeAssistant -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/alexa/test_capabilities.py b/tests/components/alexa/test_capabilities.py index 2fcc3a236e3..0bdbac70d7d 100644 --- a/tests/components/alexa/test_capabilities.py +++ b/tests/components/alexa/test_capabilities.py @@ -1,4 +1,6 @@ """Test Alexa capabilities.""" +from unittest.mock import patch + import pytest from homeassistant.components.alexa import smart_home @@ -33,7 +35,6 @@ from . import ( reported_properties, ) -from tests.async_mock import patch from tests.common import async_mock_service diff --git a/tests/components/alexa/test_entities.py b/tests/components/alexa/test_entities.py index 45991375ba0..9a1ef032762 100644 --- a/tests/components/alexa/test_entities.py +++ b/tests/components/alexa/test_entities.py @@ -1,11 +1,11 @@ """Test Alexa entity representation.""" +from unittest.mock import patch + from homeassistant.components.alexa import smart_home from homeassistant.const import __version__ from . import DEFAULT_CONFIG, get_new_request -from tests.async_mock import patch - async def test_unsupported_domain(hass): """Discovery ignores entities of unknown domains.""" diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 9a7f5760270..05a60c86ae0 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1,5 +1,7 @@ """Test for smart home alexa support.""" +from unittest.mock import patch + import pytest from homeassistant.components.alexa import messages, smart_home @@ -40,7 +42,6 @@ from . import ( reported_properties, ) -from tests.async_mock import patch from tests.common import async_mock_service diff --git a/tests/components/alexa/test_state_report.py b/tests/components/alexa/test_state_report.py index 42a8ab48279..809bca5638b 100644 --- a/tests/components/alexa/test_state_report.py +++ b/tests/components/alexa/test_state_report.py @@ -1,4 +1,7 @@ """Test report state.""" +from unittest.mock import patch + +from homeassistant import core from homeassistant.components.alexa import state_report from . import DEFAULT_CONFIG, TEST_URL @@ -171,3 +174,67 @@ async def test_doorbell_event(hass, aioclient_mock): assert call_json["event"]["header"]["name"] == "DoorbellPress" assert call_json["event"]["payload"]["cause"]["type"] == "PHYSICAL_INTERACTION" assert call_json["event"]["endpoint"]["endpointId"] == "binary_sensor#test_doorbell" + + +async def test_proactive_mode_filter_states(hass, aioclient_mock): + """Test all the cases that filter states.""" + await state_report.async_enable_proactive_mode(hass, DEFAULT_CONFIG) + + # First state should report + hass.states.async_set( + "binary_sensor.test_contact", + "on", + {"friendly_name": "Test Contact Sensor", "device_class": "door"}, + ) + assert len(aioclient_mock.mock_calls) == 0 + + aioclient_mock.clear_requests() + + # Second one shouldn't + hass.states.async_set( + "binary_sensor.test_contact", + "on", + {"friendly_name": "Test Contact Sensor", "device_class": "door"}, + ) + assert len(aioclient_mock.mock_calls) == 0 + + # hass not running should not report + hass.states.async_set( + "binary_sensor.test_contact", + "off", + {"friendly_name": "Test Contact Sensor", "device_class": "door"}, + ) + with patch.object(hass, "state", core.CoreState.stopping): + await hass.async_block_till_done() + await hass.async_block_till_done() + assert len(aioclient_mock.mock_calls) == 0 + + # unsupported entity should not report + hass.states.async_set( + "binary_sensor.test_contact", + "on", + {"friendly_name": "Test Contact Sensor", "device_class": "door"}, + ) + with patch.dict( + "homeassistant.components.alexa.state_report.ENTITY_ADAPTERS", {}, clear=True + ): + await hass.async_block_till_done() + await hass.async_block_till_done() + assert len(aioclient_mock.mock_calls) == 0 + + # Not exposed by config should not report + hass.states.async_set( + "binary_sensor.test_contact", + "off", + {"friendly_name": "Test Contact Sensor", "device_class": "door"}, + ) + with patch.object(DEFAULT_CONFIG, "should_expose", return_value=False): + await hass.async_block_till_done() + await hass.async_block_till_done() + assert len(aioclient_mock.mock_calls) == 0 + + # Removing an entity + hass.states.async_remove("binary_sensor.test_contact") + await hass.async_block_till_done() + await hass.async_block_till_done() + assert len(aioclient_mock.mock_calls) == 0 diff --git a/tests/components/almond/test_config_flow.py b/tests/components/almond/test_config_flow.py index afcad55bf2a..8f6b68e47ee 100644 --- a/tests/components/almond/test_config_flow.py +++ b/tests/components/almond/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Almond config flow.""" import asyncio +from unittest.mock import patch from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.almond import config_flow @@ -7,7 +8,6 @@ from homeassistant.components.almond.const import DOMAIN from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow -from tests.async_mock import patch from tests.common import MockConfigEntry CLIENT_ID_VALUE = "1234" diff --git a/tests/components/almond/test_init.py b/tests/components/almond/test_init.py index 35a641bac01..fb74bdfa7f5 100644 --- a/tests/components/almond/test_init.py +++ b/tests/components/almond/test_init.py @@ -1,5 +1,6 @@ """Tests for Almond set up.""" from time import time +from unittest.mock import patch import pytest @@ -10,7 +11,6 @@ from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/ambiclimate/test_config_flow.py b/tests/components/ambiclimate/test_config_flow.py index 04c714ebb5f..b87c2171815 100644 --- a/tests/components/ambiclimate/test_config_flow.py +++ b/tests/components/ambiclimate/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for the Ambiclimate config flow.""" +from unittest.mock import AsyncMock, patch + import ambiclimate from homeassistant import data_entry_flow @@ -7,8 +9,6 @@ from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.setup import async_setup_component from homeassistant.util import aiohttp -from tests.async_mock import AsyncMock, patch - async def init_config_flow(hass): """Init a configuration flow.""" diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index b5b6bbd0a46..b8ee4aaa2cd 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -1,6 +1,6 @@ """Define patches used for androidtv tests.""" -from tests.async_mock import mock_open, patch +from unittest.mock import mock_open, patch KEY_PYTHON = "python" KEY_SERVER = "server" diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index f6d189cfbc2..a9a803741b1 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -2,6 +2,7 @@ import base64 import copy import logging +from unittest.mock import patch from androidtv.constants import APPS as ANDROIDTV_APPS from androidtv.exceptions import LockNotAcquiredException @@ -58,7 +59,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.components.androidtv import patchers SHELL_RESPONSE_OFF = "" diff --git a/tests/components/apache_kafka/test_init.py b/tests/components/apache_kafka/test_init.py index ea62bb0569d..7e793bce96a 100644 --- a/tests/components/apache_kafka/test_init.py +++ b/tests/components/apache_kafka/test_init.py @@ -2,6 +2,7 @@ from asyncio import AbstractEventLoop from dataclasses import dataclass from typing import Callable, Type +from unittest.mock import patch import pytest @@ -9,8 +10,6 @@ import homeassistant.components.apache_kafka as apache_kafka from homeassistant.const import STATE_ON from homeassistant.setup import async_setup_component -from tests.async_mock import patch - APACHE_KAFKA_PATH = "homeassistant.components.apache_kafka" PRODUCER_PATH = f"{APACHE_KAFKA_PATH}.AIOKafkaProducer" MIN_CONFIG = { diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py index e54af925dec..678a8096af5 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -1,6 +1,7 @@ """The tests for the Home Assistant API component.""" # pylint: disable=protected-access import json +from unittest.mock import patch from aiohttp import web import pytest @@ -11,7 +12,6 @@ from homeassistant.bootstrap import DATA_LOGGING import homeassistant.core as ha from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import async_mock_service diff --git a/tests/components/apns/test_notify.py b/tests/components/apns/test_notify.py index 6aa327a3943..22d0a30ab16 100644 --- a/tests/components/apns/test_notify.py +++ b/tests/components/apns/test_notify.py @@ -1,5 +1,6 @@ """The tests for the APNS component.""" import io +from unittest.mock import Mock, mock_open, patch from apns2.errors import Unregistered import pytest @@ -10,7 +11,6 @@ import homeassistant.components.notify as notify from homeassistant.core import State from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, mock_open, patch from tests.common import assert_setup_component CONFIG = { diff --git a/tests/components/apple_tv/conftest.py b/tests/components/apple_tv/conftest.py index 50b57e073d9..db543007fb2 100644 --- a/tests/components/apple_tv/conftest.py +++ b/tests/components/apple_tv/conftest.py @@ -1,12 +1,12 @@ """Fixtures for component.""" +from unittest.mock import patch + from pyatv import conf, net import pytest from .common import MockPairingHandler, create_conf -from tests.async_mock import patch - @pytest.fixture(autouse=True, name="mock_scan") def mock_scan_fixture(): diff --git a/tests/components/apple_tv/test_config_flow.py b/tests/components/apple_tv/test_config_flow.py index 50344dc3c05..c55bef8edfd 100644 --- a/tests/components/apple_tv/test_config_flow.py +++ b/tests/components/apple_tv/test_config_flow.py @@ -1,5 +1,7 @@ """Test config flow.""" +from unittest.mock import patch + from pyatv import exceptions from pyatv.const import Protocol import pytest @@ -7,7 +9,6 @@ import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components.apple_tv.const import CONF_START_OFF, DOMAIN -from tests.async_mock import patch from tests.common import MockConfigEntry DMAP_SERVICE = { diff --git a/tests/components/apprise/test_notify.py b/tests/components/apprise/test_notify.py index 125971016cb..8135f4e8e2c 100644 --- a/tests/components/apprise/test_notify.py +++ b/tests/components/apprise/test_notify.py @@ -1,7 +1,7 @@ """The tests for the apprise notification platform.""" -from homeassistant.setup import async_setup_component +from unittest.mock import MagicMock, patch -from tests.async_mock import MagicMock, patch +from homeassistant.setup import async_setup_component BASE_COMPONENT = "notify" diff --git a/tests/components/aprs/test_device_tracker.py b/tests/components/aprs/test_device_tracker.py index 95cdf4befec..dc0cf09f28d 100644 --- a/tests/components/aprs/test_device_tracker.py +++ b/tests/components/aprs/test_device_tracker.py @@ -1,10 +1,11 @@ """Test APRS device tracker.""" +from unittest.mock import Mock, patch + import aprslib import homeassistant.components.aprs.device_tracker as device_tracker from homeassistant.const import EVENT_HOMEASSISTANT_START -from tests.async_mock import Mock, patch from tests.common import get_test_home_assistant DEFAULT_PORT = 14580 diff --git a/tests/components/arcam_fmj/conftest.py b/tests/components/arcam_fmj/conftest.py index dfdc9e434f2..2ef0df9511e 100644 --- a/tests/components/arcam_fmj/conftest.py +++ b/tests/components/arcam_fmj/conftest.py @@ -1,4 +1,6 @@ """Tests for the arcam_fmj component.""" +from unittest.mock import Mock, patch + from arcam.fmj.client import Client from arcam.fmj.state import State import pytest @@ -7,7 +9,6 @@ from homeassistant.components.arcam_fmj.const import DEFAULT_NAME from homeassistant.components.arcam_fmj.media_player import ArcamFmj from homeassistant.const import CONF_HOST, CONF_PORT -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry MOCK_HOST = "127.0.0.1" diff --git a/tests/components/arcam_fmj/test_config_flow.py b/tests/components/arcam_fmj/test_config_flow.py index 7de4b83723e..6c86b2bbf96 100644 --- a/tests/components/arcam_fmj/test_config_flow.py +++ b/tests/components/arcam_fmj/test_config_flow.py @@ -1,5 +1,7 @@ """Tests for the Arcam FMJ config flow module.""" +from unittest.mock import AsyncMock, patch + from arcam.fmj.client import ConnectionFailed import pytest @@ -19,7 +21,6 @@ from .conftest import ( MOCK_UUID, ) -from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry MOCK_UPNP_DEVICE = f""" diff --git a/tests/components/arcam_fmj/test_media_player.py b/tests/components/arcam_fmj/test_media_player.py index 91117cff0a2..05a070aada2 100644 --- a/tests/components/arcam_fmj/test_media_player.py +++ b/tests/components/arcam_fmj/test_media_player.py @@ -1,5 +1,6 @@ """Tests for arcam fmj receivers.""" from math import isclose +from unittest.mock import ANY, MagicMock, Mock, PropertyMock, patch from arcam.fmj import DecodeMode2CH, DecodeModeMCH, IncomingAudioFormat, SourceCodes import pytest @@ -13,8 +14,6 @@ from homeassistant.const import ATTR_ENTITY_ID from .conftest import MOCK_HOST, MOCK_NAME, MOCK_PORT, MOCK_UUID -from tests.async_mock import ANY, MagicMock, Mock, PropertyMock, patch - MOCK_TURN_ON = { "service": "switch.turn_on", "data": {"entity_id": "switch.test"}, diff --git a/tests/components/arlo/test_sensor.py b/tests/components/arlo/test_sensor.py index 85a1d1e315a..5d729a5a658 100644 --- a/tests/components/arlo/test_sensor.py +++ b/tests/components/arlo/test_sensor.py @@ -1,5 +1,6 @@ """The tests for the Netgear Arlo sensors.""" from collections import namedtuple +from unittest.mock import patch import pytest @@ -11,8 +12,6 @@ from homeassistant.const import ( PERCENTAGE, ) -from tests.async_mock import patch - def _get_named_tuple(input_dict): return namedtuple("Struct", input_dict.keys())(*input_dict.values()) diff --git a/tests/components/asuswrt/test_device_tracker.py b/tests/components/asuswrt/test_device_tracker.py index ed733b54d25..941b0c340d6 100644 --- a/tests/components/asuswrt/test_device_tracker.py +++ b/tests/components/asuswrt/test_device_tracker.py @@ -1,5 +1,7 @@ """The tests for the ASUSWRT device tracker platform.""" +from unittest.mock import AsyncMock, patch + from homeassistant.components.asuswrt import ( CONF_DNSMASQ, CONF_INTERFACE, @@ -9,8 +11,6 @@ from homeassistant.components.asuswrt import ( from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, patch - async def test_password_or_pub_key_required(hass): """Test creating an AsusWRT scanner without a pass or pubkey.""" diff --git a/tests/components/asuswrt/test_sensor.py b/tests/components/asuswrt/test_sensor.py index 7c929992473..69c70c409d5 100644 --- a/tests/components/asuswrt/test_sensor.py +++ b/tests/components/asuswrt/test_sensor.py @@ -1,5 +1,7 @@ """The tests for the AsusWrt sensor platform.""" +from unittest.mock import AsyncMock, patch + from aioasuswrt.asuswrt import Device from homeassistant.components import sensor @@ -16,8 +18,6 @@ from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, patch - VALID_CONFIG_ROUTER_SSH = { DOMAIN: { CONF_DNSMASQ: "/", diff --git a/tests/components/atag/test_climate.py b/tests/components/atag/test_climate.py index 48418050d2d..3d511821baf 100644 --- a/tests/components/atag/test_climate.py +++ b/tests/components/atag/test_climate.py @@ -1,5 +1,7 @@ """Tests for the Atag climate platform.""" +from unittest.mock import PropertyMock, patch + from homeassistant.components.atag import CLIMATE, DOMAIN from homeassistant.components.climate import ( ATTR_HVAC_ACTION, @@ -19,7 +21,6 @@ from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_UNKNOWN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.async_mock import PropertyMock, patch from tests.components.atag import UID, init_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/atag/test_config_flow.py b/tests/components/atag/test_config_flow.py index 59d8d99670c..81375792c71 100644 --- a/tests/components/atag/test_config_flow.py +++ b/tests/components/atag/test_config_flow.py @@ -1,11 +1,12 @@ """Tests for the Atag config flow.""" +from unittest.mock import PropertyMock, patch + from pyatag import errors from homeassistant import config_entries, data_entry_flow from homeassistant.components.atag import DOMAIN from homeassistant.core import HomeAssistant -from tests.async_mock import PropertyMock, patch from tests.components.atag import ( PAIR_REPLY, RECEIVE_REPLY, diff --git a/tests/components/atag/test_init.py b/tests/components/atag/test_init.py index 9f7ae9cb4ed..b86de8a8be5 100644 --- a/tests/components/atag/test_init.py +++ b/tests/components/atag/test_init.py @@ -1,11 +1,12 @@ """Tests for the ATAG integration.""" +from unittest.mock import patch + import aiohttp from homeassistant.components.atag import DOMAIN from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY from homeassistant.core import HomeAssistant -from tests.async_mock import patch from tests.components.atag import init_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/atag/test_water_heater.py b/tests/components/atag/test_water_heater.py index 0d717db70bc..5eb219fa3bc 100644 --- a/tests/components/atag/test_water_heater.py +++ b/tests/components/atag/test_water_heater.py @@ -1,11 +1,12 @@ """Tests for the Atag water heater platform.""" +from unittest.mock import patch + from homeassistant.components.atag import DOMAIN, WATER_HEATER from homeassistant.components.water_heater import SERVICE_SET_TEMPERATURE from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE from homeassistant.core import HomeAssistant -from tests.async_mock import patch from tests.components.atag import UID, init_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/august/mocks.py b/tests/components/august/mocks.py index 93b64ebbd3f..e02e1ec59dd 100644 --- a/tests/components/august/mocks.py +++ b/tests/components/august/mocks.py @@ -3,6 +3,9 @@ import json import os import time +# from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch + from august.activity import ( ACTIVITY_ACTIONS_DOOR_OPERATION, ACTIVITY_ACTIONS_DOORBELL_DING, @@ -27,8 +30,6 @@ from homeassistant.components.august import ( ) from homeassistant.setup import async_setup_component -# from tests.async_mock import AsyncMock -from tests.async_mock import AsyncMock, MagicMock, PropertyMock, patch from tests.common import load_fixture diff --git a/tests/components/august/test_binary_sensor.py b/tests/components/august/test_binary_sensor.py index 2112638e619..763f9f9528f 100644 --- a/tests/components/august/test_binary_sensor.py +++ b/tests/components/august/test_binary_sensor.py @@ -121,9 +121,7 @@ async def test_doorbell_device_registry(hass): device_registry = await hass.helpers.device_registry.async_get_registry() - reg_device = device_registry.async_get_device( - identifiers={("august", "tmt100")}, connections=set() - ) + reg_device = device_registry.async_get_device(identifiers={("august", "tmt100")}) assert reg_device.model == "hydra1" assert reg_device.name == "tmt100 Name" assert reg_device.manufacturer == "August Home Inc." diff --git a/tests/components/august/test_camera.py b/tests/components/august/test_camera.py index 3ec1b2d608c..151f7972e1e 100644 --- a/tests/components/august/test_camera.py +++ b/tests/components/august/test_camera.py @@ -1,8 +1,9 @@ """The camera tests for the august platform.""" +from unittest.mock import patch + from homeassistant.const import STATE_IDLE -from tests.async_mock import patch from tests.components.august.mocks import ( _create_august_with_devices, _mock_doorbell_from_fixture, diff --git a/tests/components/august/test_config_flow.py b/tests/components/august/test_config_flow.py index 0f9a8ebbd2f..c1e7c9bb3c5 100644 --- a/tests/components/august/test_config_flow.py +++ b/tests/components/august/test_config_flow.py @@ -1,4 +1,6 @@ """Test the August config flow.""" +from unittest.mock import patch + from august.authenticator import ValidationResult from homeassistant import config_entries, setup @@ -16,7 +18,6 @@ from homeassistant.components.august.exceptions import ( ) from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/august/test_gateway.py b/tests/components/august/test_gateway.py index c1aa0723baa..ced07360008 100644 --- a/tests/components/august/test_gateway.py +++ b/tests/components/august/test_gateway.py @@ -1,10 +1,11 @@ """The gateway tests for the august platform.""" +from unittest.mock import MagicMock, patch + from august.authenticator_common import AuthenticationState from homeassistant.components.august.const import DOMAIN from homeassistant.components.august.gateway import AugustGateway -from tests.async_mock import MagicMock, patch from tests.components.august.mocks import _mock_august_authentication, _mock_get_config diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index f954ff83c25..e881ac09c97 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -1,5 +1,6 @@ """The tests for the august platform.""" import asyncio +from unittest.mock import patch from aiohttp import ClientResponseError from august.authenticator_common import AuthenticationState @@ -31,7 +32,6 @@ from homeassistant.const import ( from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry from tests.components.august.mocks import ( _create_august_with_devices, diff --git a/tests/components/august/test_lock.py b/tests/components/august/test_lock.py index f36a5e3f180..d013da30ff6 100644 --- a/tests/components/august/test_lock.py +++ b/tests/components/august/test_lock.py @@ -6,7 +6,6 @@ from homeassistant.const import ( SERVICE_LOCK, SERVICE_UNLOCK, STATE_LOCKED, - STATE_UNAVAILABLE, STATE_UNKNOWN, STATE_UNLOCKED, ) @@ -27,7 +26,7 @@ async def test_lock_device_registry(hass): device_registry = await hass.helpers.device_registry.async_get_registry() reg_device = device_registry.async_get_device( - identifiers={("august", "online_with_doorsense")}, connections=set() + identifiers={("august", "online_with_doorsense")} ) assert reg_device.model == "AUG-MD01" assert reg_device.sw_version == "undefined-4.3.0-1.8.14" @@ -98,7 +97,7 @@ async def test_one_lock_operation(hass): assert lock_operator_sensor assert ( hass.states.get("sensor.online_with_doorsense_name_operator").state - == STATE_UNAVAILABLE + == STATE_UNKNOWN ) diff --git a/tests/components/august/test_sensor.py b/tests/components/august/test_sensor.py index 51e00b9d09f..fb7ddfde979 100644 --- a/tests/components/august/test_sensor.py +++ b/tests/components/august/test_sensor.py @@ -1,6 +1,6 @@ """The sensor tests for the august platform.""" -from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, STATE_UNAVAILABLE +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, STATE_UNKNOWN from tests.components.august.mocks import ( _create_august_with_devices, @@ -120,7 +120,7 @@ async def test_create_lock_with_low_battery_linked_keypad(hass): ) assert ( hass.states.get("sensor.a6697750d607098bae8d6baa11ef8063_name_operator").state - == STATE_UNAVAILABLE + == STATE_UNKNOWN ) diff --git a/tests/components/aurora/test_config_flow.py b/tests/components/aurora/test_config_flow.py index 2f4b457a9dd..b9e0496f668 100644 --- a/tests/components/aurora/test_config_flow.py +++ b/tests/components/aurora/test_config_flow.py @@ -1,9 +1,10 @@ """Test the Aurora config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.aurora.const import DOMAIN -from tests.async_mock import patch from tests.common import MockConfigEntry DATA = { diff --git a/tests/components/auth/test_indieauth.py b/tests/components/auth/test_indieauth.py index e8aabce4678..4cf7402725d 100644 --- a/tests/components/auth/test_indieauth.py +++ b/tests/components/auth/test_indieauth.py @@ -1,11 +1,11 @@ """Tests for the client validator.""" import asyncio +from unittest.mock import patch import pytest from homeassistant.components.auth import indieauth -from tests.async_mock import patch from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index 3d799fe0078..2c9a39c6fb6 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -1,5 +1,6 @@ """Integration tests for the auth component.""" from datetime import timedelta +from unittest.mock import patch from homeassistant.auth.models import Credentials from homeassistant.components import auth @@ -9,7 +10,6 @@ from homeassistant.util.dt import utcnow from . import async_setup_auth -from tests.async_mock import patch from tests.common import CLIENT_ID, CLIENT_REDIRECT_URI, MockUser diff --git a/tests/components/auth/test_login_flow.py b/tests/components/auth/test_login_flow.py index f2629a27bb9..e6e5281d601 100644 --- a/tests/components/auth/test_login_flow.py +++ b/tests/components/auth/test_login_flow.py @@ -1,7 +1,8 @@ """Tests for the login flow.""" +from unittest.mock import patch + from . import async_setup_auth -from tests.async_mock import patch from tests.common import CLIENT_ID, CLIENT_REDIRECT_URI diff --git a/tests/components/automation/test_blueprint.py b/tests/components/automation/test_blueprint.py index 56062af17b7..1a6ccec7a8e 100644 --- a/tests/components/automation/test_blueprint.py +++ b/tests/components/automation/test_blueprint.py @@ -3,6 +3,7 @@ import asyncio import contextlib from datetime import timedelta import pathlib +from unittest.mock import patch from homeassistant.components import automation from homeassistant.components.blueprint import models @@ -10,7 +11,6 @@ from homeassistant.core import callback from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util, yaml -from tests.async_mock import patch from tests.common import async_fire_time_changed, async_mock_service BUILTIN_BLUEPRINT_FOLDER = pathlib.Path(automation.__file__).parent / "blueprints" diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 5f258fc28b7..c31af555e32 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1,5 +1,6 @@ """The tests for the automation component.""" import asyncio +from unittest.mock import Mock, patch import pytest @@ -28,7 +29,6 @@ from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import Mock, patch from tests.common import assert_setup_component, async_mock_service, mock_restore_cache from tests.components.logbook.test_init import MockLazyEventPartialState @@ -947,6 +947,7 @@ async def test_automation_with_error_in_script(hass, caplog): hass.bus.async_fire("test_event") await hass.async_block_till_done() assert "Service not found" in caplog.text + assert "Traceback" not in caplog.text async def test_automation_with_error_in_script_2(hass, caplog): diff --git a/tests/components/awair/test_config_flow.py b/tests/components/awair/test_config_flow.py index 6dcbc7eac9e..92d92dd5a63 100644 --- a/tests/components/awair/test_config_flow.py +++ b/tests/components/awair/test_config_flow.py @@ -1,5 +1,7 @@ """Define tests for the Awair config flow.""" +from unittest.mock import patch + from python_awair.exceptions import AuthError, AwairError from homeassistant import data_entry_flow @@ -9,7 +11,6 @@ from homeassistant.const import CONF_ACCESS_TOKEN from .const import CONFIG, DEVICES_FIXTURE, NO_DEVICES_FIXTURE, UNIQUE_ID, USER_FIXTURE -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/awair/test_sensor.py b/tests/components/awair/test_sensor.py index 3b013fad29c..0fcbab99a3a 100644 --- a/tests/components/awair/test_sensor.py +++ b/tests/components/awair/test_sensor.py @@ -1,5 +1,7 @@ """Tests for the Awair sensor platform.""" +from unittest.mock import patch + from homeassistant.components.awair.const import ( API_CO2, API_HUMID, @@ -40,7 +42,6 @@ from .const import ( USER_FIXTURE, ) -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/aws/test_init.py b/tests/components/aws/test_init.py index 045ad2ff609..e50c0aa546b 100644 --- a/tests/components/aws/test_init.py +++ b/tests/components/aws/test_init.py @@ -1,9 +1,9 @@ """Tests for the aws component config and setup.""" +from unittest.mock import AsyncMock, MagicMock, patch as async_patch + from homeassistant.components import aws from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, MagicMock, patch as async_patch - class MockAioSession: """Mock AioSession.""" diff --git a/tests/components/axis/test_binary_sensor.py b/tests/components/axis/test_binary_sensor.py index 0c6f4535cd0..98ef55282c3 100644 --- a/tests/components/axis/test_binary_sensor.py +++ b/tests/components/axis/test_binary_sensor.py @@ -19,6 +19,14 @@ EVENTS = [ "type": "state", "value": "0", }, + { + "operation": "Initialized", + "topic": "tns1:PTZController/tnsaxis:PTZPresets/Channel_1", + "source": "PresetToken", + "source_idx": "0", + "type": "on_preset", + "value": "1", + }, { "operation": "Initialized", "topic": "tnsaxis:CameraApplicationPlatform/VMD/Camera1Profile1", @@ -54,8 +62,7 @@ async def test_binary_sensors(hass): config_entry = await setup_axis_integration(hass) device = hass.data[AXIS_DOMAIN][config_entry.unique_id] - for event in EVENTS: - device.api.event.process_event(event) + device.api.event.update(EVENTS) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 2 diff --git a/tests/components/axis/test_camera.py b/tests/components/axis/test_camera.py index 01c5a677e38..4961b4c40ca 100644 --- a/tests/components/axis/test_camera.py +++ b/tests/components/axis/test_camera.py @@ -1,5 +1,7 @@ """Axis camera platform tests.""" +from unittest.mock import patch + from homeassistant.components import camera from homeassistant.components.axis.const import ( CONF_STREAM_PROFILE, @@ -11,8 +13,6 @@ from homeassistant.setup import async_setup_component from .test_device import ENTRY_OPTIONS, NAME, setup_axis_integration -from tests.async_mock import patch - async def test_platform_manually_configured(hass): """Test that nothing happens when platform is manually configured.""" @@ -64,7 +64,7 @@ async def test_camera_with_stream_profile(hass): assert camera_entity.image_source == "http://1.2.3.4:80/axis-cgi/jpg/image.cgi" assert ( camera_entity.mjpeg_source - == "http://1.2.3.4:80/axis-cgi/mjpg/video.cgi?&streamprofile=profile_1" + == "http://1.2.3.4:80/axis-cgi/mjpg/video.cgi?streamprofile=profile_1" ) assert ( await camera_entity.stream_source() diff --git a/tests/components/axis/test_config_flow.py b/tests/components/axis/test_config_flow.py index b9dceec7477..7d62e999809 100644 --- a/tests/components/axis/test_config_flow.py +++ b/tests/components/axis/test_config_flow.py @@ -1,17 +1,31 @@ """Test Axis config flow.""" +from unittest.mock import patch + +import pytest +import respx + from homeassistant import data_entry_flow from homeassistant.components.axis import config_flow from homeassistant.components.axis.const import ( CONF_EVENTS, CONF_MODEL, CONF_STREAM_PROFILE, + CONF_VIDEO_SOURCE, DEFAULT_STREAM_PROFILE, + DEFAULT_VIDEO_SOURCE, DOMAIN as AXIS_DOMAIN, ) -from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS +from homeassistant.config_entries import ( + SOURCE_DHCP, + SOURCE_IGNORE, + SOURCE_REAUTH, + SOURCE_SSDP, + SOURCE_USER, + SOURCE_ZEROCONF, +) from homeassistant.const import ( CONF_HOST, - CONF_MAC, CONF_NAME, CONF_PASSWORD, CONF_PORT, @@ -23,9 +37,15 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) -from .test_device import MAC, MODEL, NAME, setup_axis_integration, vapix_request +from .test_device import ( + DEFAULT_HOST, + MAC, + MODEL, + NAME, + mock_default_vapix_requests, + setup_axis_integration, +) -from tests.async_mock import patch from tests.common import MockConfigEntry @@ -40,7 +60,8 @@ async def test_flow_manual_configuration(hass): assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == SOURCE_USER - with patch("axis.vapix.Vapix.request", new=vapix_request): + with respx.mock: + mock_default_vapix_requests(respx) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -58,7 +79,6 @@ async def test_flow_manual_configuration(hass): CONF_USERNAME: "user", CONF_PASSWORD: "pass", CONF_PORT: 80, - CONF_MAC: MAC, CONF_MODEL: "M1065-LW", CONF_NAME: "M1065-LW 0", } @@ -79,7 +99,8 @@ async def test_manual_configuration_update_configuration(hass): with patch( "homeassistant.components.axis.async_setup_entry", return_value=True, - ) as mock_setup_entry, patch("axis.vapix.Vapix.request", new=vapix_request): + ) as mock_setup_entry, respx.mock: + mock_default_vapix_requests(respx, "2.3.4.5") result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -97,32 +118,6 @@ async def test_manual_configuration_update_configuration(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_flow_fails_already_configured(hass): - """Test that config flow fails on already configured device.""" - await setup_axis_integration(hass) - - result = await hass.config_entries.flow.async_init( - AXIS_DOMAIN, context={"source": SOURCE_USER} - ) - - assert result["type"] == RESULT_TYPE_FORM - assert result["step_id"] == SOURCE_USER - - with patch("axis.vapix.Vapix.request", new=vapix_request): - result = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "user", - CONF_PASSWORD: "pass", - CONF_PORT: 80, - }, - ) - - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - async def test_flow_fails_faulty_credentials(hass): """Test that config flow fails on faulty credentials.""" result = await hass.config_entries.flow.async_init( @@ -195,7 +190,8 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model(hass): assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == SOURCE_USER - with patch("axis.vapix.Vapix.request", new=vapix_request): + with respx.mock: + mock_default_vapix_requests(respx) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -213,7 +209,6 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model(hass): CONF_USERNAME: "user", CONF_PASSWORD: "pass", CONF_PORT: 80, - CONF_MAC: MAC, CONF_MODEL: "M1065-LW", CONF_NAME: "M1065-LW 2", } @@ -221,23 +216,107 @@ async def test_flow_create_entry_multiple_existing_entries_of_same_model(hass): assert result["data"][CONF_NAME] == "M1065-LW 2" -async def test_zeroconf_flow(hass): - """Test that zeroconf discovery for new devices work.""" +async def test_reauth_flow_update_configuration(hass): + """Test that config flow fails on already configured device.""" + config_entry = await setup_axis_integration(hass) + device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + result = await hass.config_entries.flow.async_init( AXIS_DOMAIN, - data={ - CONF_HOST: "1.2.3.4", - CONF_PORT: 80, - "hostname": "name", - "properties": {"macaddress": MAC}, - }, - context={"source": SOURCE_ZEROCONF}, + context={"source": SOURCE_REAUTH}, + data=config_entry.data, ) assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == SOURCE_USER - with patch("axis.vapix.Vapix.request", new=vapix_request): + with respx.mock: + mock_default_vapix_requests(respx, "2.3.4.5") + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "2.3.4.5", + CONF_USERNAME: "user2", + CONF_PASSWORD: "pass2", + CONF_PORT: 80, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert device.host == "2.3.4.5" + assert device.username == "user2" + assert device.password == "pass2" + + +@pytest.mark.parametrize( + "source,discovery_info", + [ + ( + SOURCE_DHCP, + { + HOSTNAME: f"axis-{MAC}", + IP_ADDRESS: DEFAULT_HOST, + MAC_ADDRESS: MAC, + }, + ), + ( + SOURCE_SSDP, + { + "st": "urn:axis-com:service:BasicService:1", + "usn": f"uuid:Upnp-BasicDevice-1_0-{MAC}::urn:axis-com:service:BasicService:1", + "ext": "", + "server": "Linux/4.14.173-axis8, UPnP/1.0, Portable SDK for UPnP devices/1.8.7", + "deviceType": "urn:schemas-upnp-org:device:Basic:1", + "friendlyName": f"AXIS M1065-LW - {MAC}", + "manufacturer": "AXIS", + "manufacturerURL": "http://www.axis.com/", + "modelDescription": "AXIS M1065-LW Network Camera", + "modelName": "AXIS M1065-LW", + "modelNumber": "M1065-LW", + "modelURL": "http://www.axis.com/", + "serialNumber": MAC, + "UDN": f"uuid:Upnp-BasicDevice-1_0-{MAC}", + "serviceList": { + "service": { + "serviceType": "urn:axis-com:service:BasicService:1", + "serviceId": "urn:axis-com:serviceId:BasicServiceId", + "controlURL": "/upnp/control/BasicServiceId", + "eventSubURL": "/upnp/event/BasicServiceId", + "SCPDURL": "/scpd_basic.xml", + } + }, + "presentationURL": f"http://{DEFAULT_HOST}:80/", + }, + ), + ( + SOURCE_ZEROCONF, + { + "host": DEFAULT_HOST, + "port": 80, + "hostname": f"axis-{MAC.lower()}.local.", + "type": "_axis-video._tcp.local.", + "name": f"AXIS M1065-LW - {MAC}._axis-video._tcp.local.", + "properties": { + "_raw": {"macaddress": MAC.encode()}, + "macaddress": MAC, + }, + }, + ), + ], +) +async def test_discovery_flow(hass, source: str, discovery_info: dict): + """Test the different discovery flows for new devices work.""" + result = await hass.config_entries.flow.async_init( + AXIS_DOMAIN, data=discovery_info, context={"source": source} + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == SOURCE_USER + + with respx.mock: + mock_default_vapix_requests(respx) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={ @@ -255,7 +334,6 @@ async def test_zeroconf_flow(hass): CONF_USERNAME: "user", CONF_PASSWORD: "pass", CONF_PORT: 80, - CONF_MAC: MAC, CONF_MODEL: "M1065-LW", CONF_NAME: "M1065-LW 0", } @@ -263,39 +341,95 @@ async def test_zeroconf_flow(hass): assert result["data"][CONF_NAME] == "M1065-LW 0" -async def test_zeroconf_flow_already_configured(hass): - """Test that zeroconf doesn't setup already configured devices.""" +@pytest.mark.parametrize( + "source,discovery_info", + [ + ( + SOURCE_DHCP, + { + HOSTNAME: f"axis-{MAC}", + IP_ADDRESS: DEFAULT_HOST, + MAC_ADDRESS: MAC, + }, + ), + ( + SOURCE_SSDP, + { + "friendlyName": f"AXIS M1065-LW - {MAC}", + "serialNumber": MAC, + "presentationURL": f"http://{DEFAULT_HOST}:80/", + }, + ), + ( + SOURCE_ZEROCONF, + { + CONF_HOST: DEFAULT_HOST, + CONF_PORT: 80, + "name": f"AXIS M1065-LW - {MAC}._axis-video._tcp.local.", + "properties": {"macaddress": MAC}, + }, + ), + ], +) +async def test_discovered_device_already_configured( + hass, source: str, discovery_info: dict +): + """Test that discovery doesn't setup already configured devices.""" config_entry = await setup_axis_integration(hass) - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] - assert device.host == "1.2.3.4" + assert config_entry.data[CONF_HOST] == DEFAULT_HOST result = await hass.config_entries.flow.async_init( - AXIS_DOMAIN, - data={ - CONF_HOST: "1.2.3.4", - CONF_PORT: 80, - "hostname": "name", - "properties": {"macaddress": MAC}, - }, - context={"source": SOURCE_ZEROCONF}, + AXIS_DOMAIN, data=discovery_info, context={"source": source} ) assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" - assert device.host == "1.2.3.4" + assert config_entry.data[CONF_HOST] == DEFAULT_HOST -async def test_zeroconf_flow_updated_configuration(hass): - """Test that zeroconf update configuration with new parameters.""" +@pytest.mark.parametrize( + "source,discovery_info,expected_port", + [ + ( + SOURCE_DHCP, + { + HOSTNAME: f"axis-{MAC}", + IP_ADDRESS: "2.3.4.5", + MAC_ADDRESS: MAC, + }, + 80, + ), + ( + SOURCE_SSDP, + { + "friendlyName": f"AXIS M1065-LW - {MAC}", + "serialNumber": MAC, + "presentationURL": "http://2.3.4.5:8080/", + }, + 8080, + ), + ( + SOURCE_ZEROCONF, + { + CONF_HOST: "2.3.4.5", + CONF_PORT: 8080, + "name": f"AXIS M1065-LW - {MAC}._axis-video._tcp.local.", + "properties": {"macaddress": MAC}, + }, + 8080, + ), + ], +) +async def test_discovery_flow_updated_configuration( + hass, source: str, discovery_info: dict, expected_port: int +): + """Test that discovery flow update configuration with new parameters.""" config_entry = await setup_axis_integration(hass) - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] - assert device.host == "1.2.3.4" - assert device.config_entry.data == { - CONF_HOST: "1.2.3.4", + assert config_entry.data == { + CONF_HOST: DEFAULT_HOST, CONF_PORT: 80, CONF_USERNAME: "root", CONF_PASSWORD: "pass", - CONF_MAC: MAC, CONF_MODEL: MODEL, CONF_NAME: NAME, } @@ -303,51 +437,100 @@ async def test_zeroconf_flow_updated_configuration(hass): with patch( "homeassistant.components.axis.async_setup_entry", return_value=True, - ) as mock_setup_entry, patch("axis.vapix.Vapix.request", new=vapix_request): + ) as mock_setup_entry, respx.mock: + mock_default_vapix_requests(respx, "2.3.4.5") result = await hass.config_entries.flow.async_init( - AXIS_DOMAIN, - data={ - CONF_HOST: "2.3.4.5", - CONF_PORT: 8080, - "hostname": "name", - "properties": {"macaddress": MAC}, - }, - context={"source": SOURCE_ZEROCONF}, + AXIS_DOMAIN, data=discovery_info, context={"source": source} ) await hass.async_block_till_done() assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" - assert device.config_entry.data == { + assert config_entry.data == { CONF_HOST: "2.3.4.5", - CONF_PORT: 8080, + CONF_PORT: expected_port, CONF_USERNAME: "root", CONF_PASSWORD: "pass", - CONF_MAC: MAC, CONF_MODEL: MODEL, CONF_NAME: NAME, } assert len(mock_setup_entry.mock_calls) == 1 -async def test_zeroconf_flow_ignore_non_axis_device(hass): - """Test that zeroconf doesn't setup devices with link local addresses.""" +@pytest.mark.parametrize( + "source,discovery_info", + [ + ( + SOURCE_DHCP, + { + HOSTNAME: "", + IP_ADDRESS: "", + MAC_ADDRESS: "01234567890", + }, + ), + ( + SOURCE_SSDP, + { + "friendlyName": "", + "serialNumber": "01234567890", + "presentationURL": "", + }, + ), + ( + SOURCE_ZEROCONF, + { + CONF_HOST: "", + CONF_PORT: 0, + "name": "", + "properties": {"macaddress": "01234567890"}, + }, + ), + ], +) +async def test_discovery_flow_ignore_non_axis_device( + hass, source: str, discovery_info: dict +): + """Test that discovery flow ignores devices with non Axis OUI.""" result = await hass.config_entries.flow.async_init( - AXIS_DOMAIN, - data={CONF_HOST: "169.254.3.4", "properties": {"macaddress": "01234567890"}}, - context={"source": SOURCE_ZEROCONF}, + AXIS_DOMAIN, data=discovery_info, context={"source": source} ) assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "not_axis_device" -async def test_zeroconf_flow_ignore_link_local_address(hass): - """Test that zeroconf doesn't setup devices with link local addresses.""" +@pytest.mark.parametrize( + "source,discovery_info", + [ + ( + SOURCE_DHCP, + {HOSTNAME: f"axis-{MAC}", IP_ADDRESS: "169.254.3.4", MAC_ADDRESS: MAC}, + ), + ( + SOURCE_SSDP, + { + "friendlyName": f"AXIS M1065-LW - {MAC}", + "serialNumber": MAC, + "presentationURL": "http://169.254.3.4:80/", + }, + ), + ( + SOURCE_ZEROCONF, + { + CONF_HOST: "169.254.3.4", + CONF_PORT: 80, + "name": f"AXIS M1065-LW - {MAC}._axis-video._tcp.local.", + "properties": {"macaddress": MAC}, + }, + ), + ], +) +async def test_discovery_flow_ignore_link_local_address( + hass, source: str, discovery_info: dict +): + """Test that discovery flow ignores devices with link local addresses.""" result = await hass.config_entries.flow.async_init( - AXIS_DOMAIN, - data={CONF_HOST: "169.254.3.4", "properties": {"macaddress": MAC}}, - context={"source": SOURCE_ZEROCONF}, + AXIS_DOMAIN, data=discovery_info, context={"source": source} ) assert result["type"] == RESULT_TYPE_ABORT @@ -359,8 +542,13 @@ async def test_option_flow(hass): config_entry = await setup_axis_integration(hass) device = hass.data[AXIS_DOMAIN][config_entry.unique_id] assert device.option_stream_profile == DEFAULT_STREAM_PROFILE + assert device.option_video_source == DEFAULT_VIDEO_SOURCE - result = await hass.config_entries.options.async_init(device.config_entry.entry_id) + with respx.mock: + mock_default_vapix_requests(respx) + result = await hass.config_entries.options.async_init( + device.config_entry.entry_id + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "configure_stream" @@ -369,15 +557,21 @@ async def test_option_flow(hass): "profile_1", "profile_2", } + assert set(result["data_schema"].schema[CONF_VIDEO_SOURCE].container) == { + DEFAULT_VIDEO_SOURCE, + 1, + } result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={CONF_STREAM_PROFILE: "profile_1"}, + user_input={CONF_STREAM_PROFILE: "profile_1", CONF_VIDEO_SOURCE: 1}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"] == { CONF_EVENTS: True, CONF_STREAM_PROFILE: "profile_1", + CONF_VIDEO_SOURCE: 1, } assert device.option_stream_profile == "profile_1" + assert device.option_video_source == 1 diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index b7612a01c1d..a5371395638 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -1,25 +1,12 @@ """Test Axis device.""" from copy import deepcopy from unittest import mock +from unittest.mock import Mock, patch import axis as axislib -from axis.api_discovery import URL as API_DISCOVERY_URL -from axis.applications import URL_LIST as APPLICATIONS_URL -from axis.applications.vmd4 import URL as VMD4_URL -from axis.basic_device_info import URL as BASIC_DEVICE_INFO_URL from axis.event_stream import OPERATION_INITIALIZED -from axis.light_control import URL as LIGHT_CONTROL_URL -from axis.mqtt import URL_CLIENT as MQTT_CLIENT_URL -from axis.param_cgi import ( - BRAND as BRAND_URL, - INPUT as INPUT_URL, - IOPORT as IOPORT_URL, - OUTPUT as OUTPUT_URL, - PROPERTIES as PROPERTIES_URL, - STREAM_PROFILES as STREAM_PROFILES_URL, -) -from axis.port_management import URL as PORT_MANAGEMENT_URL import pytest +import respx from homeassistant import config_entries from homeassistant.components import axis @@ -32,7 +19,6 @@ from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAI from homeassistant.config_entries import SOURCE_ZEROCONF from homeassistant.const import ( CONF_HOST, - CONF_MAC, CONF_NAME, CONF_PASSWORD, CONF_PORT, @@ -40,21 +26,22 @@ from homeassistant.const import ( STATE_ON, ) -from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry, async_fire_mqtt_message -MAC = "00408C12345" +MAC = "00408C123456" +FORMATTED_MAC = "00:40:8c:12:34:56" MODEL = "model" NAME = "name" +DEFAULT_HOST = "1.2.3.4" + ENTRY_OPTIONS = {CONF_EVENTS: True} ENTRY_CONFIG = { - CONF_HOST: "1.2.3.4", + CONF_HOST: DEFAULT_HOST, CONF_USERNAME: "root", CONF_PASSWORD: "pass", CONF_PORT: 80, - CONF_MAC: MAC, CONF_MODEL: MODEL, CONF_NAME: NAME, } @@ -92,7 +79,7 @@ BASIC_DEVICE_INFO_RESPONSE = { "propertyList": { "ProdNbr": "M1065-LW", "ProdType": "Network Camera", - "SerialNumber": "00408C12345", + "SerialNumber": MAC, "Version": "9.80.1", } }, @@ -166,6 +153,14 @@ root.Brand.ProdVariant= root.Brand.WebURL=http://www.axis.com """ +IMAGE_RESPONSE = """root.Image.I0.Enabled=yes +root.Image.I0.Name=View Area 1 +root.Image.I0.Source=0 +root.Image.I1.Enabled=no +root.Image.I1.Name=View Area 2 +root.Image.I1.Source=0 +""" + PORTS_RESPONSE = """root.Input.NbrOfInputs=1 root.IOPort.I0.Configurable=no root.IOPort.I0.Direction=input @@ -174,7 +169,7 @@ root.IOPort.I0.Input.Trig=closed root.Output.NbrOfOutputs=0 """ -PROPERTIES_RESPONSE = """root.Properties.API.HTTP.Version=3 +PROPERTIES_RESPONSE = f"""root.Properties.API.HTTP.Version=3 root.Properties.API.Metadata.Metadata=yes root.Properties.API.Metadata.Version=1.0 root.Properties.EmbeddedDevelopment.Version=2.16 @@ -185,9 +180,12 @@ root.Properties.Image.Format=jpeg,mjpeg,h264 root.Properties.Image.NbrOfViews=2 root.Properties.Image.Resolution=1920x1080,1280x960,1280x720,1024x768,1024x576,800x600,640x480,640x360,352x240,320x240 root.Properties.Image.Rotation=0,180 -root.Properties.System.SerialNumber=00408C12345 +root.Properties.System.SerialNumber={MAC} """ +PTZ_RESPONSE = "" + + STREAM_PROFILES_RESPONSE = """root.StreamProfile.MaxGroups=26 root.StreamProfile.S0.Description=profile_1_description root.StreamProfile.S0.Name=profile_1 @@ -197,31 +195,85 @@ root.StreamProfile.S1.Name=profile_2 root.StreamProfile.S1.Parameters=videocodec=h265 """ +VIEW_AREAS_RESPONSE = {"apiVersion": "1.0", "method": "list", "data": {"viewAreas": []}} -async def vapix_request(self, session, url, **kwargs): - """Return data based on url.""" - if API_DISCOVERY_URL in url: - return API_DISCOVERY_RESPONSE - if APPLICATIONS_URL in url: - return APPLICATIONS_LIST_RESPONSE - if BASIC_DEVICE_INFO_URL in url: - return BASIC_DEVICE_INFO_RESPONSE - if LIGHT_CONTROL_URL in url: - return LIGHT_CONTROL_RESPONSE - if MQTT_CLIENT_URL in url: - return MQTT_CLIENT_RESPONSE - if PORT_MANAGEMENT_URL in url: - return PORT_MANAGEMENT_RESPONSE - if VMD4_URL in url: - return VMD4_RESPONSE - if BRAND_URL in url: - return BRAND_RESPONSE - if IOPORT_URL in url or INPUT_URL in url or OUTPUT_URL in url: - return PORTS_RESPONSE - if PROPERTIES_URL in url: - return PROPERTIES_RESPONSE - if STREAM_PROFILES_URL in url: - return STREAM_PROFILES_RESPONSE + +def mock_default_vapix_requests(respx: respx, host: str = DEFAULT_HOST) -> None: + """Mock default Vapix requests responses.""" + respx.post(f"http://{host}:80/axis-cgi/apidiscovery.cgi").respond( + json=API_DISCOVERY_RESPONSE, + ) + respx.post(f"http://{host}:80/axis-cgi/basicdeviceinfo.cgi").respond( + json=BASIC_DEVICE_INFO_RESPONSE, + ) + respx.post(f"http://{host}:80/axis-cgi/io/portmanagement.cgi").respond( + json=PORT_MANAGEMENT_RESPONSE, + ) + respx.post(f"http://{host}:80/axis-cgi/lightcontrol.cgi").respond( + json=LIGHT_CONTROL_RESPONSE, + ) + respx.post(f"http://{host}:80/axis-cgi/mqtt/client.cgi").respond( + json=MQTT_CLIENT_RESPONSE, + ) + respx.post(f"http://{host}:80/axis-cgi/streamprofile.cgi").respond( + json=STREAM_PROFILES_RESPONSE, + ) + respx.post(f"http://{host}:80/axis-cgi/viewarea/info.cgi").respond( + json=VIEW_AREAS_RESPONSE + ) + respx.get( + f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.Brand" + ).respond( + text=BRAND_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.get( + f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.Image" + ).respond( + text=IMAGE_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.get( + f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.Input" + ).respond( + text=PORTS_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.get( + f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.IOPort" + ).respond( + text=PORTS_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.get( + f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.Output" + ).respond( + text=PORTS_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.get( + f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.Properties" + ).respond( + text=PROPERTIES_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.get( + f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.PTZ" + ).respond( + text=PTZ_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.get( + f"http://{host}:80/axis-cgi/param.cgi?action=list&group=root.StreamProfile" + ).respond( + text=STREAM_PROFILES_RESPONSE, + headers={"Content-Type": "text/plain"}, + ) + respx.post(f"http://{host}:80/axis-cgi/applications/list.cgi").respond( + text=APPLICATIONS_LIST_RESPONSE, + headers={"Content-Type": "text/xml"}, + ) + respx.post(f"http://{host}:80/local/vmd/control.cgi").respond(json=VMD4_RESPONSE) async def setup_axis_integration(hass, config=ENTRY_CONFIG, options=ENTRY_OPTIONS): @@ -231,14 +283,13 @@ async def setup_axis_integration(hass, config=ENTRY_CONFIG, options=ENTRY_OPTION data=deepcopy(config), connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, options=deepcopy(options), - version=2, + version=3, + unique_id=FORMATTED_MAC, ) config_entry.add_to_hass(hass) - with patch("axis.vapix.Vapix.request", new=vapix_request), patch( - "axis.rtsp.RTSPClient.start", - return_value=True, - ): + with patch("axis.rtsp.RTSPClient.start", return_value=True), respx.mock: + mock_default_vapix_requests(respx) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -257,7 +308,7 @@ async def test_device_setup(hass): assert device.api.vapix.firmware_version == "9.10.1" assert device.api.vapix.product_number == "M1065-LW" assert device.api.vapix.product_type == "Network Camera" - assert device.api.vapix.serial_number == "00408C12345" + assert device.api.vapix.serial_number == "00408C123456" entry = device.config_entry @@ -270,7 +321,7 @@ async def test_device_setup(hass): assert device.host == ENTRY_CONFIG[CONF_HOST] assert device.model == ENTRY_CONFIG[CONF_MODEL] assert device.name == ENTRY_CONFIG[CONF_NAME] - assert device.serial == ENTRY_CONFIG[CONF_MAC] + assert device.unique_id == FORMATTED_MAC async def test_device_info(hass): @@ -285,7 +336,7 @@ async def test_device_info(hass): assert device.api.vapix.firmware_version == "9.80.1" assert device.api.vapix.product_number == "M1065-LW" assert device.api.vapix.product_type == "Network Camera" - assert device.api.vapix.serial_number == "00408C12345" + assert device.api.vapix.serial_number == "00408C123456" async def test_device_support_mqtt(hass, mqtt_mock): @@ -317,16 +368,17 @@ async def test_update_address(hass): device = hass.data[AXIS_DOMAIN][config_entry.unique_id] assert device.api.config.host == "1.2.3.4" - with patch("axis.vapix.Vapix.request", new=vapix_request), patch( + with patch( "homeassistant.components.axis.async_setup_entry", return_value=True, - ) as mock_setup_entry: + ) as mock_setup_entry, respx.mock: + mock_default_vapix_requests(respx, "2.3.4.5") await hass.config_entries.flow.async_init( AXIS_DOMAIN, data={ "host": "2.3.4.5", "port": 80, - "hostname": "name", + "name": "name", "properties": {"macaddress": MAC}, }, context={"source": SOURCE_ZEROCONF}, @@ -360,6 +412,16 @@ async def test_device_not_accessible(hass): assert hass.data[AXIS_DOMAIN] == {} +async def test_device_trigger_reauth_flow(hass): + """Failed authentication trigger a reauthentication flow.""" + with patch.object( + axis.device, "get_device", side_effect=axis.errors.AuthenticationRequired + ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: + await setup_axis_integration(hass) + mock_flow_init.assert_called_once() + assert hass.data[AXIS_DOMAIN] == {} + + async def test_device_unknown_error(hass): """Unknown errors are handled.""" with patch.object(axis.device, "get_device", side_effect=Exception): @@ -390,12 +452,10 @@ async def test_shutdown(): axis_device = axis.device.AxisNetworkDevice(hass, entry) axis_device.api = Mock() - axis_device.api.vapix.close = AsyncMock() await axis_device.shutdown(None) assert len(axis_device.api.stream.stop.mock_calls) == 1 - assert len(axis_device.api.vapix.close.mock_calls) == 1 async def test_get_device_fails(hass): diff --git a/tests/components/axis/test_init.py b/tests/components/axis/test_init.py index cf5253d4675..b7faceaf10d 100644 --- a/tests/components/axis/test_init.py +++ b/tests/components/axis/test_init.py @@ -1,6 +1,9 @@ """Test Axis component setup process.""" +from unittest.mock import AsyncMock, Mock, patch + from homeassistant.components import axis from homeassistant.components.axis.const import CONF_MODEL, DOMAIN as AXIS_DOMAIN +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.const import ( CONF_DEVICE, CONF_HOST, @@ -10,11 +13,12 @@ from homeassistant.const import ( CONF_PORT, CONF_USERNAME, ) +from homeassistant.helpers import entity_registry +from homeassistant.helpers.device_registry import format_mac from homeassistant.setup import async_setup_component from .test_device import MAC, setup_axis_integration -from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry @@ -28,13 +32,13 @@ async def test_setup_entry(hass): """Test successful setup of entry.""" await setup_axis_integration(hass) assert len(hass.data[AXIS_DOMAIN]) == 1 - assert MAC in hass.data[AXIS_DOMAIN] + assert format_mac(MAC) in hass.data[AXIS_DOMAIN] async def test_setup_entry_fails(hass): """Test successful setup of entry.""" config_entry = MockConfigEntry( - domain=AXIS_DOMAIN, data={CONF_MAC: "0123"}, version=2 + domain=AXIS_DOMAIN, data={CONF_MAC: "0123"}, version=3 ) config_entry.add_to_hass(hass) @@ -68,7 +72,7 @@ async def test_migrate_entry(hass): CONF_PASSWORD: "password", CONF_PORT: 80, }, - CONF_MAC: "mac", + CONF_MAC: "00408C123456", CONF_MODEL: "model", CONF_NAME: "name", } @@ -76,6 +80,17 @@ async def test_migrate_entry(hass): assert entry.data == legacy_config assert entry.version == 1 + assert not entry.unique_id + + # Create entity entry to migrate to new unique ID + registry = await entity_registry.async_get_registry(hass) + registry.async_get_or_create( + BINARY_SENSOR_DOMAIN, + AXIS_DOMAIN, + "00408C123456-vmd4-0", + suggested_object_id="vmd4", + config_entry=entry, + ) await entry.async_migrate(hass) @@ -90,8 +105,12 @@ async def test_migrate_entry(hass): CONF_USERNAME: "username", CONF_PASSWORD: "password", CONF_PORT: 80, - CONF_MAC: "mac", + CONF_MAC: "00408C123456", CONF_MODEL: "model", CONF_NAME: "name", } - assert entry.version == 2 + assert entry.version == 3 + assert entry.unique_id == "00:40:8c:12:34:56" + + vmd4_entity = registry.async_get("binary_sensor.vmd4") + assert vmd4_entity.unique_id == "00:40:8c:12:34:56-vmd4-0" diff --git a/tests/components/axis/test_light.py b/tests/components/axis/test_light.py index 06612daa313..db4ba86ceae 100644 --- a/tests/components/axis/test_light.py +++ b/tests/components/axis/test_light.py @@ -1,6 +1,7 @@ """Axis light platform tests.""" from copy import deepcopy +from unittest.mock import patch from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN as LIGHT_DOMAIN @@ -13,9 +14,12 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from .test_device import API_DISCOVERY_RESPONSE, NAME, setup_axis_integration - -from tests.async_mock import patch +from .test_device import ( + API_DISCOVERY_RESPONSE, + LIGHT_CONTROL_RESPONSE, + NAME, + setup_axis_integration, +) API_DISCOVERY_LIGHT_CONTROL = { "id": "light-control", @@ -58,6 +62,26 @@ async def test_no_lights(hass): assert not hass.states.async_entity_ids(LIGHT_DOMAIN) +async def test_no_light_entity_without_light_control_representation(hass): + """Verify no lights entities get created without light control representation.""" + api_discovery = deepcopy(API_DISCOVERY_RESPONSE) + api_discovery["data"]["apiList"].append(API_DISCOVERY_LIGHT_CONTROL) + + light_control = deepcopy(LIGHT_CONTROL_RESPONSE) + light_control["data"]["items"] = [] + + with patch.dict(API_DISCOVERY_RESPONSE, api_discovery), patch.dict( + LIGHT_CONTROL_RESPONSE, light_control + ): + config_entry = await setup_axis_integration(hass) + device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + + device.api.event.update([EVENT_ON]) + await hass.async_block_till_done() + + assert not hass.states.async_entity_ids(LIGHT_DOMAIN) + + async def test_lights(hass): """Test that lights are loaded properly.""" api_discovery = deepcopy(API_DISCOVERY_RESPONSE) @@ -75,7 +99,7 @@ async def test_lights(hass): "axis.light_control.LightControl.get_valid_intensity", return_value={"data": {"ranges": [{"high": 150}]}}, ): - device.api.event.process_event(EVENT_ON) + device.api.event.update([EVENT_ON]) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(LIGHT_DOMAIN)) == 1 @@ -120,7 +144,7 @@ async def test_lights(hass): mock_deactivate.assert_called_once() # Event turn off light - device.api.event.process_event(EVENT_OFF) + device.api.event.update([EVENT_OFF]) await hass.async_block_till_done() light_0 = hass.states.get(entity_id) diff --git a/tests/components/axis/test_switch.py b/tests/components/axis/test_switch.py index fbcf0624fc9..dcbe285cb54 100644 --- a/tests/components/axis/test_switch.py +++ b/tests/components/axis/test_switch.py @@ -1,6 +1,7 @@ """Axis switch platform tests.""" from copy import deepcopy +from unittest.mock import AsyncMock, patch from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN @@ -20,8 +21,6 @@ from .test_device import ( setup_axis_integration, ) -from tests.async_mock import AsyncMock, patch - EVENTS = [ { "operation": "Initialized", @@ -69,8 +68,7 @@ async def test_switches_with_port_cgi(hass): device.api.vapix.ports["0"].close = AsyncMock() device.api.vapix.ports["1"].name = "" - for event in EVENTS: - device.api.event.process_event(event) + device.api.event.update(EVENTS) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 @@ -117,8 +115,7 @@ async def test_switches_with_port_management(hass): device.api.vapix.ports["0"].close = AsyncMock() device.api.vapix.ports["1"].name = "" - for event in EVENTS: - device.api.event.process_event(event) + device.api.event.update(EVENTS) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 diff --git a/tests/components/azure_devops/test_config_flow.py b/tests/components/azure_devops/test_config_flow.py index daad5992350..4cda9076b98 100644 --- a/tests/components/azure_devops/test_config_flow.py +++ b/tests/components/azure_devops/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Azure DevOps config flow.""" +from unittest.mock import patch + from aioazuredevops.core import DevOpsProject import aiohttp @@ -11,7 +13,6 @@ from homeassistant.components.azure_devops.const import ( ) from homeassistant.core import HomeAssistant -from tests.async_mock import patch from tests.common import MockConfigEntry FIXTURE_REAUTH_INPUT = {CONF_PAT: "abc123"} diff --git a/tests/components/azure_event_hub/test_init.py b/tests/components/azure_event_hub/test_init.py index 42bfefbcb3c..dd588ad7499 100644 --- a/tests/components/azure_event_hub/test_init.py +++ b/tests/components/azure_event_hub/test_init.py @@ -1,5 +1,6 @@ """The tests for the Azure Event Hub component.""" from dataclasses import dataclass +from unittest.mock import MagicMock, patch import pytest @@ -7,8 +8,6 @@ import homeassistant.components.azure_event_hub as azure_event_hub from homeassistant.const import STATE_ON from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, patch - AZURE_EVENT_HUB_PATH = "homeassistant.components.azure_event_hub" PRODUCER_PATH = f"{AZURE_EVENT_HUB_PATH}.EventHubProducerClient" MIN_CONFIG = { diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py index 2be01679777..01f2664ea67 100644 --- a/tests/components/bayesian/test_binary_sensor.py +++ b/tests/components/bayesian/test_binary_sensor.py @@ -1,6 +1,7 @@ """The test for the bayesian sensor platform.""" import json from os import path +from unittest.mock import patch from homeassistant import config as hass_config from homeassistant.components.bayesian import DOMAIN, binary_sensor as bayesian @@ -18,8 +19,6 @@ from homeassistant.const import ( from homeassistant.core import Context, callback from homeassistant.setup import async_setup_component -from tests.async_mock import patch - async def test_load_values_when_added_to_hass(hass): """Test that sensor initializes with observations of relevant entities.""" diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py index 7a7e80e36a3..f25b529d426 100644 --- a/tests/components/binary_sensor/test_device_condition.py +++ b/tests/components/binary_sensor/test_device_condition.py @@ -1,5 +1,6 @@ """The test for binary_sensor device automation.""" from datetime import timedelta +from unittest.mock import patch import pytest @@ -11,7 +12,6 @@ from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import ( MockConfigEntry, async_get_device_automation_capabilities, diff --git a/tests/components/blebox/conftest.py b/tests/components/blebox/conftest.py index e43fb9fc4b4..3b12c682f3f 100644 --- a/tests/components/blebox/conftest.py +++ b/tests/components/blebox/conftest.py @@ -1,5 +1,6 @@ """PyTest fixtures and test helpers.""" from unittest import mock +from unittest.mock import AsyncMock, PropertyMock, patch import blebox_uniapi import pytest @@ -8,7 +9,6 @@ from homeassistant.components.blebox.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, PropertyMock, patch from tests.common import MockConfigEntry from tests.components.light.conftest import mock_light_profiles # noqa diff --git a/tests/components/blebox/test_air_quality.py b/tests/components/blebox/test_air_quality.py index 3467c94411c..4f1f6dff671 100644 --- a/tests/components/blebox/test_air_quality.py +++ b/tests/components/blebox/test_air_quality.py @@ -1,6 +1,7 @@ """Blebox air_quality tests.""" import logging +from unittest.mock import AsyncMock, PropertyMock import blebox_uniapi import pytest @@ -10,8 +11,6 @@ from homeassistant.const import ATTR_ICON, STATE_UNKNOWN from .conftest import async_setup_entity, mock_feature -from tests.async_mock import AsyncMock, PropertyMock - @pytest.fixture(name="airsensor") def airsensor_fixture(): diff --git a/tests/components/blebox/test_climate.py b/tests/components/blebox/test_climate.py index c36c93a7f98..baaa5a5009e 100644 --- a/tests/components/blebox/test_climate.py +++ b/tests/components/blebox/test_climate.py @@ -1,6 +1,7 @@ """BleBox climate entities tests.""" import logging +from unittest.mock import AsyncMock, PropertyMock import blebox_uniapi import pytest @@ -30,8 +31,6 @@ from homeassistant.const import ( from .conftest import async_setup_entity, mock_feature -from tests.async_mock import AsyncMock, PropertyMock - @pytest.fixture(name="saunabox") def saunabox_fixture(): diff --git a/tests/components/blebox/test_config_flow.py b/tests/components/blebox/test_config_flow.py index a7200b05b28..965c707d2af 100644 --- a/tests/components/blebox/test_config_flow.py +++ b/tests/components/blebox/test_config_flow.py @@ -1,5 +1,7 @@ """Test Home Assistant config flow for BleBox devices.""" +from unittest.mock import DEFAULT, AsyncMock, PropertyMock, patch + import blebox_uniapi import pytest @@ -9,8 +11,6 @@ from homeassistant.setup import async_setup_component from .conftest import mock_config, mock_only_feature, setup_product_mock -from tests.async_mock import DEFAULT, AsyncMock, PropertyMock, patch - def create_valid_feature_mock(path="homeassistant.components.blebox.Products"): """Return a valid, complete BleBox feature mock.""" diff --git a/tests/components/blebox/test_cover.py b/tests/components/blebox/test_cover.py index cd060445bf7..a5d3a8f705b 100644 --- a/tests/components/blebox/test_cover.py +++ b/tests/components/blebox/test_cover.py @@ -1,6 +1,7 @@ """BleBox cover entities tests.""" import logging +from unittest.mock import AsyncMock, PropertyMock import blebox_uniapi import pytest @@ -32,8 +33,6 @@ from homeassistant.const import ( from .conftest import async_setup_entity, mock_feature -from tests.async_mock import AsyncMock, PropertyMock - ALL_COVER_FIXTURES = ["gatecontroller", "shutterbox", "gatebox"] FIXTURES_SUPPORTING_STOP = ["gatecontroller", "shutterbox"] diff --git a/tests/components/blebox/test_light.py b/tests/components/blebox/test_light.py index 48af534545c..5d9e5709e4d 100644 --- a/tests/components/blebox/test_light.py +++ b/tests/components/blebox/test_light.py @@ -1,6 +1,7 @@ """BleBox light entities tests.""" import logging +from unittest.mock import AsyncMock, PropertyMock import blebox_uniapi import pytest @@ -24,8 +25,6 @@ from homeassistant.util import color from .conftest import async_setup_entity, mock_feature -from tests.async_mock import AsyncMock, PropertyMock - ALL_LIGHT_FIXTURES = ["dimmer", "wlightbox_s", "wlightbox"] diff --git a/tests/components/blebox/test_sensor.py b/tests/components/blebox/test_sensor.py index a19e628181c..aeb726cc726 100644 --- a/tests/components/blebox/test_sensor.py +++ b/tests/components/blebox/test_sensor.py @@ -1,6 +1,7 @@ """Blebox sensors tests.""" import logging +from unittest.mock import AsyncMock, PropertyMock import blebox_uniapi import pytest @@ -15,8 +16,6 @@ from homeassistant.const import ( from .conftest import async_setup_entity, mock_feature -from tests.async_mock import AsyncMock, PropertyMock - @pytest.fixture(name="tempsensor") def tempsensor_fixture(): diff --git a/tests/components/blebox/test_switch.py b/tests/components/blebox/test_switch.py index 43d7d811acc..e2bc1240510 100644 --- a/tests/components/blebox/test_switch.py +++ b/tests/components/blebox/test_switch.py @@ -1,6 +1,7 @@ """Blebox switch tests.""" import logging +from unittest.mock import AsyncMock, PropertyMock import blebox_uniapi import pytest @@ -22,8 +23,6 @@ from .conftest import ( setup_product_mock, ) -from tests.async_mock import AsyncMock, PropertyMock - @pytest.fixture(name="switchbox") def switchbox_fixture(): diff --git a/tests/components/blink/test_config_flow.py b/tests/components/blink/test_config_flow.py index 36e3fbd95ea..91264997769 100644 --- a/tests/components/blink/test_config_flow.py +++ b/tests/components/blink/test_config_flow.py @@ -1,11 +1,12 @@ """Test the Blink config flow.""" +from unittest.mock import Mock, patch + from blinkpy.auth import LoginError from blinkpy.blinkpy import BlinkSetupError from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.blink import DOMAIN -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/blueprint/conftest.py b/tests/components/blueprint/conftest.py index c8110ddaf08..ec76451065c 100644 --- a/tests/components/blueprint/conftest.py +++ b/tests/components/blueprint/conftest.py @@ -1,8 +1,8 @@ """Blueprints conftest.""" -import pytest +from unittest.mock import patch -from tests.async_mock import patch +import pytest @pytest.fixture(autouse=True) diff --git a/tests/components/blueprint/test_models.py b/tests/components/blueprint/test_models.py index 6e15bd952a4..ba8914c3b1d 100644 --- a/tests/components/blueprint/test_models.py +++ b/tests/components/blueprint/test_models.py @@ -1,13 +1,12 @@ """Test blueprint models.""" import logging +from unittest.mock import patch import pytest from homeassistant.components.blueprint import errors, models from homeassistant.util.yaml import Input -from tests.async_mock import patch - @pytest.fixture def blueprint_1(): diff --git a/tests/components/blueprint/test_websocket_api.py b/tests/components/blueprint/test_websocket_api.py index bb08414b6e8..6dfd445c634 100644 --- a/tests/components/blueprint/test_websocket_api.py +++ b/tests/components/blueprint/test_websocket_api.py @@ -1,12 +1,11 @@ """Test websocket API.""" from pathlib import Path +from unittest.mock import Mock, patch import pytest from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch - @pytest.fixture(autouse=True) async def setup_bp(hass): diff --git a/tests/components/bluetooth_le_tracker/test_device_tracker.py b/tests/components/bluetooth_le_tracker/test_device_tracker.py index af4df463339..308371c9aaa 100644 --- a/tests/components/bluetooth_le_tracker/test_device_tracker.py +++ b/tests/components/bluetooth_le_tracker/test_device_tracker.py @@ -1,6 +1,7 @@ """Test Bluetooth LE device tracker.""" from datetime import timedelta +from unittest.mock import patch from homeassistant.components.bluetooth_le_tracker import device_tracker from homeassistant.components.device_tracker.const import ( @@ -12,7 +13,6 @@ from homeassistant.const import CONF_PLATFORM from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util, slugify -from tests.async_mock import patch from tests.common import async_fire_time_changed diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py index ae32feec7b1..52433f2f58f 100644 --- a/tests/components/bmw_connected_drive/test_config_flow.py +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -1,4 +1,6 @@ """Test the for the BMW Connected Drive config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, data_entry_flow from homeassistant.components.bmw_connected_drive.config_flow import DOMAIN from homeassistant.components.bmw_connected_drive.const import ( @@ -8,7 +10,6 @@ from homeassistant.components.bmw_connected_drive.const import ( ) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry FIXTURE_USER_INPUT = { diff --git a/tests/components/bond/common.py b/tests/components/bond/common.py index d5e4a0b9418..9aaaf9a249d 100644 --- a/tests/components/bond/common.py +++ b/tests/components/bond/common.py @@ -3,6 +3,7 @@ from asyncio import TimeoutError as AsyncIOTimeoutError from contextlib import nullcontext from datetime import timedelta from typing import Any, Dict, Optional +from unittest.mock import patch from homeassistant import core from homeassistant.components.bond.const import DOMAIN as BOND_DOMAIN @@ -10,7 +11,6 @@ from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, STATE_UNAVAILABLE from homeassistant.setup import async_setup_component from homeassistant.util import utcnow -from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/bond/test_config_flow.py b/tests/components/bond/test_config_flow.py index b87891a1896..dba6c590641 100644 --- a/tests/components/bond/test_config_flow.py +++ b/tests/components/bond/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Bond config flow.""" from typing import Any, Dict +from unittest.mock import Mock, patch from aiohttp import ClientConnectionError, ClientResponseError @@ -9,7 +10,6 @@ from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST from .common import patch_bond_device_ids, patch_bond_version -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/bond/test_init.py b/tests/components/bond/test_init.py index 3dd54f0de6f..98d86058c49 100644 --- a/tests/components/bond/test_init.py +++ b/tests/components/bond/test_init.py @@ -70,9 +70,7 @@ async def test_async_setup_entry_sets_up_hub_and_supported_domains(hass: HomeAss # verify hub device is registered correctly device_registry = await dr.async_get_registry(hass) - hub = device_registry.async_get_device( - identifiers={(DOMAIN, "test-bond-id")}, connections=set() - ) + hub = device_registry.async_get_device(identifiers={(DOMAIN, "test-bond-id")}) assert hub.name == "test-bond-id" assert hub.manufacturer == "Olibra" assert hub.model == "test-model" diff --git a/tests/components/braviatv/test_config_flow.py b/tests/components/braviatv/test_config_flow.py index c6f76105cfc..cbe87f14839 100644 --- a/tests/components/braviatv/test_config_flow.py +++ b/tests/components/braviatv/test_config_flow.py @@ -1,4 +1,6 @@ """Define tests for the Bravia TV config flow.""" +from unittest.mock import patch + from bravia_tv.braviarc import NoIPControl from homeassistant import data_entry_flow @@ -6,7 +8,6 @@ from homeassistant.components.braviatv.const import CONF_IGNORED_SOURCES, DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_MAC, CONF_PIN -from tests.async_mock import patch from tests.common import MockConfigEntry BRAVIA_SYSTEM_INFO = { diff --git a/tests/components/broadlink/__init__.py b/tests/components/broadlink/__init__.py index 86756c922f1..7185c605f5c 100644 --- a/tests/components/broadlink/__init__.py +++ b/tests/components/broadlink/__init__.py @@ -1,7 +1,8 @@ """Tests for the Broadlink integration.""" +from unittest.mock import MagicMock, patch + from homeassistant.components.broadlink.const import DOMAIN -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry # Do not edit/remove. Adding is ok. diff --git a/tests/components/broadlink/test_config_flow.py b/tests/components/broadlink/test_config_flow.py index c19d831cf3a..30f19c178b7 100644 --- a/tests/components/broadlink/test_config_flow.py +++ b/tests/components/broadlink/test_config_flow.py @@ -1,6 +1,7 @@ """Test the Broadlink config flow.""" import errno import socket +from unittest.mock import call, patch import broadlink.exceptions as blke import pytest @@ -10,8 +11,6 @@ from homeassistant.components.broadlink.const import DOMAIN from . import get_device -from tests.async_mock import call, patch - DEVICE_DISCOVERY = "homeassistant.components.broadlink.config_flow.blk.discover" DEVICE_FACTORY = "homeassistant.components.broadlink.config_flow.blk.gendevice" diff --git a/tests/components/broadlink/test_device.py b/tests/components/broadlink/test_device.py index d267243aeb9..df22bcaffcb 100644 --- a/tests/components/broadlink/test_device.py +++ b/tests/components/broadlink/test_device.py @@ -1,4 +1,6 @@ """Tests for Broadlink devices.""" +from unittest.mock import patch + import broadlink.exceptions as blke from homeassistant.components.broadlink.const import DOMAIN @@ -13,7 +15,6 @@ from homeassistant.helpers.entity_registry import async_entries_for_device from . import get_device -from tests.async_mock import patch from tests.common import mock_device_registry, mock_registry @@ -252,9 +253,7 @@ async def test_device_setup_registry(hass): assert len(device_registry.devices) == 1 - device_entry = device_registry.async_get_device( - {(DOMAIN, mock_entry.unique_id)}, set() - ) + device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) assert device_entry.identifiers == {(DOMAIN, device.mac)} assert device_entry.name == device.name assert device_entry.model == device.model @@ -338,9 +337,7 @@ async def test_device_update_listener(hass): hass.config_entries.async_update_entry(mock_entry, title="New Name") await hass.async_block_till_done() - device_entry = device_registry.async_get_device( - {(DOMAIN, mock_entry.unique_id)}, set() - ) + device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) assert device_entry.name == "New Name" for entry in async_entries_for_device(entity_registry, device_entry.id): assert entry.original_name.startswith("New Name") diff --git a/tests/components/broadlink/test_remote.py b/tests/components/broadlink/test_remote.py index 941dfc4d3ce..2d21b588c33 100644 --- a/tests/components/broadlink/test_remote.py +++ b/tests/components/broadlink/test_remote.py @@ -1,5 +1,6 @@ """Tests for Broadlink remotes.""" from base64 import b64decode +from unittest.mock import call from homeassistant.components.broadlink.const import DOMAIN, REMOTE_DOMAIN from homeassistant.components.remote import ( @@ -12,7 +13,6 @@ from homeassistant.helpers.entity_registry import async_entries_for_device from . import get_device -from tests.async_mock import call from tests.common import mock_device_registry, mock_registry REMOTE_DEVICES = ["Entrance", "Living Room", "Office", "Garage"] @@ -31,7 +31,7 @@ async def test_remote_setup_works(hass): mock_api, mock_entry = await device.setup_entry(hass) device_entry = device_registry.async_get_device( - {(DOMAIN, mock_entry.unique_id)}, set() + {(DOMAIN, mock_entry.unique_id)} ) entries = async_entries_for_device(entity_registry, device_entry.id) remotes = {entry for entry in entries if entry.domain == REMOTE_DOMAIN} @@ -51,7 +51,7 @@ async def test_remote_send_command(hass): mock_api, mock_entry = await device.setup_entry(hass) device_entry = device_registry.async_get_device( - {(DOMAIN, mock_entry.unique_id)}, set() + {(DOMAIN, mock_entry.unique_id)} ) entries = async_entries_for_device(entity_registry, device_entry.id) remotes = {entry for entry in entries if entry.domain == REMOTE_DOMAIN} @@ -78,7 +78,7 @@ async def test_remote_turn_off_turn_on(hass): mock_api, mock_entry = await device.setup_entry(hass) device_entry = device_registry.async_get_device( - {(DOMAIN, mock_entry.unique_id)}, set() + {(DOMAIN, mock_entry.unique_id)} ) entries = async_entries_for_device(entity_registry, device_entry.id) remotes = {entry for entry in entries if entry.domain == REMOTE_DOMAIN} diff --git a/tests/components/broadlink/test_sensors.py b/tests/components/broadlink/test_sensors.py index a7d6a304654..de0cd88f288 100644 --- a/tests/components/broadlink/test_sensors.py +++ b/tests/components/broadlink/test_sensors.py @@ -25,9 +25,7 @@ async def test_a1_sensor_setup(hass): mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) assert mock_api.check_sensors_raw.call_count == 1 - device_entry = device_registry.async_get_device( - {(DOMAIN, mock_entry.unique_id)}, set() - ) + device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} assert len(sensors) == 5 @@ -62,9 +60,7 @@ async def test_a1_sensor_update(hass): mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) - device_entry = device_registry.async_get_device( - {(DOMAIN, mock_entry.unique_id)}, set() - ) + device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} assert len(sensors) == 5 @@ -106,9 +102,7 @@ async def test_rm_pro_sensor_setup(hass): mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) assert mock_api.check_sensors.call_count == 1 - device_entry = device_registry.async_get_device( - {(DOMAIN, mock_entry.unique_id)}, set() - ) + device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} assert len(sensors) == 1 @@ -131,9 +125,7 @@ async def test_rm_pro_sensor_update(hass): mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) - device_entry = device_registry.async_get_device( - {(DOMAIN, mock_entry.unique_id)}, set() - ) + device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} assert len(sensors) == 1 @@ -163,9 +155,7 @@ async def test_rm_mini3_no_sensor(hass): mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) assert mock_api.check_sensors.call_count <= 1 - device_entry = device_registry.async_get_device( - {(DOMAIN, mock_entry.unique_id)}, set() - ) + device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} assert len(sensors) == 0 @@ -183,9 +173,7 @@ async def test_rm4_pro_hts2_sensor_setup(hass): mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) assert mock_api.check_sensors.call_count == 1 - device_entry = device_registry.async_get_device( - {(DOMAIN, mock_entry.unique_id)}, set() - ) + device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} assert len(sensors) == 2 @@ -211,9 +199,7 @@ async def test_rm4_pro_hts2_sensor_update(hass): mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) - device_entry = device_registry.async_get_device( - {(DOMAIN, mock_entry.unique_id)}, set() - ) + device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} assert len(sensors) == 2 @@ -246,9 +232,7 @@ async def test_rm4_pro_no_sensor(hass): mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) assert mock_api.check_sensors.call_count <= 1 - device_entry = device_registry.async_get_device( - {(DOMAIN, mock_entry.unique_id)}, set() - ) + device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} assert len(sensors) == 0 diff --git a/tests/components/brother/__init__.py b/tests/components/brother/__init__.py index b24ef97705b..b4706d56fba 100644 --- a/tests/components/brother/__init__.py +++ b/tests/components/brother/__init__.py @@ -1,10 +1,10 @@ """Tests for Brother Printer integration.""" import json +from unittest.mock import patch from homeassistant.components.brother.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_TYPE -from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py index 153c07deeac..d681ac9c988 100644 --- a/tests/components/brother/test_config_flow.py +++ b/tests/components/brother/test_config_flow.py @@ -1,5 +1,6 @@ """Define tests for the Brother Printer config flow.""" import json +from unittest.mock import patch from brother import SnmpError, UnsupportedModel @@ -8,7 +9,6 @@ from homeassistant.components.brother.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_TYPE -from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture CONFIG = {CONF_HOST: "localhost", CONF_TYPE: "laser"} diff --git a/tests/components/brother/test_init.py b/tests/components/brother/test_init.py index 04c3c130fc9..7b85586ce28 100644 --- a/tests/components/brother/test_init.py +++ b/tests/components/brother/test_init.py @@ -1,4 +1,6 @@ """Test init of Brother integration.""" +from unittest.mock import patch + from homeassistant.components.brother.const import DOMAIN from homeassistant.config_entries import ( ENTRY_STATE_LOADED, @@ -7,7 +9,6 @@ from homeassistant.config_entries import ( ) from homeassistant.const import CONF_HOST, CONF_TYPE, STATE_UNAVAILABLE -from tests.async_mock import patch from tests.common import MockConfigEntry from tests.components.brother import init_integration diff --git a/tests/components/brother/test_sensor.py b/tests/components/brother/test_sensor.py index 3f9ee9394b7..b386b0753b7 100644 --- a/tests/components/brother/test_sensor.py +++ b/tests/components/brother/test_sensor.py @@ -1,6 +1,7 @@ """Test sensor of Brother integration.""" from datetime import datetime, timedelta import json +from unittest.mock import Mock, patch from homeassistant.components.brother.const import DOMAIN, UNIT_PAGES from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -16,7 +17,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component from homeassistant.util.dt import UTC, utcnow -from tests.async_mock import Mock, patch from tests.common import async_fire_time_changed, load_fixture from tests.components.brother import init_integration diff --git a/tests/components/caldav/test_calendar.py b/tests/components/caldav/test_calendar.py index 1b6c21c7358..3e380b44de4 100644 --- a/tests/components/caldav/test_calendar.py +++ b/tests/components/caldav/test_calendar.py @@ -1,5 +1,6 @@ """The tests for the webdav calendar component.""" import datetime +from unittest.mock import MagicMock, Mock, patch from caldav.objects import Event import pytest @@ -8,8 +9,6 @@ from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component from homeassistant.util import dt -from tests.async_mock import MagicMock, Mock, patch - # pylint: disable=redefined-outer-name DEVICE_DATA = {"name": "Private Calendar", "device_id": "Private Calendar"} diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 966adc97b67..2c2d744deb9 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -2,6 +2,7 @@ import asyncio import base64 import io +from unittest.mock import Mock, PropertyMock, mock_open, patch import pytest @@ -14,7 +15,6 @@ from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, PropertyMock, mock_open, patch from tests.components.camera import common diff --git a/tests/components/canary/__init__.py b/tests/components/canary/__init__.py index 9d0e488d516..27cec31b9e9 100644 --- a/tests/components/canary/__init__.py +++ b/tests/components/canary/__init__.py @@ -1,5 +1,5 @@ """Tests for the Canary integration.""" -from unittest.mock import MagicMock, PropertyMock +from unittest.mock import MagicMock, PropertyMock, patch from canary.api import SensorType @@ -12,7 +12,6 @@ from homeassistant.components.canary.const import ( from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import patch from tests.common import MockConfigEntry ENTRY_CONFIG = { diff --git a/tests/components/canary/conftest.py b/tests/components/canary/conftest.py index ed0e7f80f8d..26db0dfefcf 100644 --- a/tests/components/canary/conftest.py +++ b/tests/components/canary/conftest.py @@ -1,9 +1,9 @@ """Define fixtures available for all tests.""" +from unittest.mock import MagicMock, patch + from canary.api import Api from pytest import fixture -from tests.async_mock import MagicMock, patch - @fixture(autouse=True) def mock_ffmpeg(hass): diff --git a/tests/components/canary/test_alarm_control_panel.py b/tests/components/canary/test_alarm_control_panel.py index 930fd9613e0..a21284ec376 100644 --- a/tests/components/canary/test_alarm_control_panel.py +++ b/tests/components/canary/test_alarm_control_panel.py @@ -1,4 +1,6 @@ """The tests for the Canary alarm_control_panel platform.""" +from unittest.mock import PropertyMock, patch + from canary.api import LOCATION_MODE_AWAY, LOCATION_MODE_HOME, LOCATION_MODE_NIGHT from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN @@ -18,7 +20,6 @@ from homeassistant.setup import async_setup_component from . import mock_device, mock_location, mock_mode -from tests.async_mock import PropertyMock, patch from tests.common import mock_registry diff --git a/tests/components/canary/test_config_flow.py b/tests/components/canary/test_config_flow.py index e02217dc67e..d194ae21185 100644 --- a/tests/components/canary/test_config_flow.py +++ b/tests/components/canary/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Canary config flow.""" +from unittest.mock import patch + from requests import ConnectTimeout, HTTPError from homeassistant.components.canary.const import ( @@ -18,8 +20,6 @@ from homeassistant.setup import async_setup_component from . import USER_INPUT, _patch_async_setup, _patch_async_setup_entry, init_integration -from tests.async_mock import patch - async def test_user_form(hass, canary_config_flow): """Test we get the user initiated form.""" diff --git a/tests/components/canary/test_init.py b/tests/components/canary/test_init.py index f548a007505..a767eb0ec51 100644 --- a/tests/components/canary/test_init.py +++ b/tests/components/canary/test_init.py @@ -1,4 +1,6 @@ """The tests for the Canary component.""" +from unittest.mock import patch + from requests import ConnectTimeout from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN @@ -13,8 +15,6 @@ from homeassistant.setup import async_setup_component from . import YAML_CONFIG, init_integration -from tests.async_mock import patch - async def test_import_from_yaml(hass, canary) -> None: """Test import from YAML.""" diff --git a/tests/components/canary/test_sensor.py b/tests/components/canary/test_sensor.py index d32741d3705..6419f81a62e 100644 --- a/tests/components/canary/test_sensor.py +++ b/tests/components/canary/test_sensor.py @@ -1,5 +1,6 @@ """The tests for the Canary sensor platform.""" from datetime import timedelta +from unittest.mock import patch from homeassistant.components.canary.const import DOMAIN, MANUFACTURER from homeassistant.components.canary.sensor import ( @@ -23,7 +24,6 @@ from homeassistant.util.dt import utcnow from . import mock_device, mock_location, mock_reading -from tests.async_mock import patch from tests.common import async_fire_time_changed, mock_device_registry, mock_registry @@ -88,7 +88,7 @@ async def test_sensors_pro(hass, canary) -> None: assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == data[2] assert state.state == data[1] - device = device_registry.async_get_device({(DOMAIN, "20")}, set()) + device = device_registry.async_get_device({(DOMAIN, "20")}) assert device assert device.manufacturer == MANUFACTURER assert device.name == "Dining Room" @@ -206,7 +206,7 @@ async def test_sensors_flex(hass, canary) -> None: assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == data[2] assert state.state == data[1] - device = device_registry.async_get_device({(DOMAIN, "20")}, set()) + device = device_registry.async_get_device({(DOMAIN, "20")}) assert device assert device.manufacturer == MANUFACTURER assert device.name == "Dining Room" diff --git a/tests/components/cast/test_home_assistant_cast.py b/tests/components/cast/test_home_assistant_cast.py index 8ddb6e82eda..3fd0e921ca6 100644 --- a/tests/components/cast/test_home_assistant_cast.py +++ b/tests/components/cast/test_home_assistant_cast.py @@ -1,10 +1,11 @@ """Test Home Assistant Cast.""" +from unittest.mock import patch + from homeassistant import config_entries from homeassistant.components.cast import home_assistant_cast from homeassistant.config import async_process_ha_core_config -from tests.async_mock import patch from tests.common import MockConfigEntry, async_mock_signal diff --git a/tests/components/cast/test_init.py b/tests/components/cast/test_init.py index 24be4d53ee6..d364256b703 100644 --- a/tests/components/cast/test_init.py +++ b/tests/components/cast/test_init.py @@ -1,11 +1,11 @@ """Tests for the Cast config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, data_entry_flow from homeassistant.components import cast from homeassistant.setup import async_setup_component -from tests.async_mock import patch - async def test_creating_entry_sets_up_media_player(hass): """Test setting up Cast loads the media player.""" diff --git a/tests/components/cast/test_media_player.py b/tests/components/cast/test_media_player.py index 4f75e93faef..050d6a6932d 100644 --- a/tests/components/cast/test_media_player.py +++ b/tests/components/cast/test_media_player.py @@ -2,6 +2,7 @@ # pylint: disable=protected-access import json from typing import Optional +from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch from uuid import UUID import attr @@ -13,20 +14,40 @@ from homeassistant.components.cast.media_player import ChromecastInfo from homeassistant.config import async_process_ha_core_config from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component -from tests.async_mock import ANY, AsyncMock, MagicMock, Mock, patch from tests.common import MockConfigEntry, assert_setup_component from tests.components.media_player import common +@pytest.fixture() +def dial_mock(): + """Mock pychromecast dial.""" + dial_mock = MagicMock() + dial_mock.get_device_status.return_value.uuid = "fake_uuid" + dial_mock.get_device_status.return_value.manufacturer = "fake_manufacturer" + dial_mock.get_device_status.return_value.model_name = "fake_model_name" + dial_mock.get_device_status.return_value.friendly_name = "fake_friendly_name" + dial_mock.get_multizone_status.return_value.dynamic_groups = [] + return dial_mock + + @pytest.fixture() def mz_mock(): """Mock pychromecast MultizoneManager.""" return MagicMock() +@pytest.fixture() +def pycast_mock(): + """Mock pychromecast.""" + pycast_mock = MagicMock() + pycast_mock.start_discovery.return_value = (None, Mock()) + return pycast_mock + + @pytest.fixture() def quick_play_mock(): """Mock pychromecast quick_play.""" @@ -34,20 +55,14 @@ def quick_play_mock(): @pytest.fixture(autouse=True) -def cast_mock(mz_mock, quick_play_mock): +def cast_mock(dial_mock, mz_mock, pycast_mock, quick_play_mock): """Mock pychromecast.""" - pycast_mock = MagicMock() - pycast_mock.start_discovery.return_value = (None, Mock()) - dial_mock = MagicMock(name="XXX") - dial_mock.get_device_status.return_value.uuid = "fake_uuid" - dial_mock.get_device_status.return_value.manufacturer = "fake_manufacturer" - dial_mock.get_device_status.return_value.model_name = "fake_model_name" - dial_mock.get_device_status.return_value.friendly_name = "fake_friendly_name" - with patch( "homeassistant.components.cast.media_player.pychromecast", pycast_mock ), patch( "homeassistant.components.cast.discovery.pychromecast", pycast_mock + ), patch( + "homeassistant.components.cast.helpers.dial", dial_mock ), patch( "homeassistant.components.cast.media_player.MultizoneManager", return_value=mz_mock, @@ -130,6 +145,7 @@ async def async_setup_cast_internal_discovery(hass, config=None): assert start_discovery.call_count == 1 discovery_callback = cast_listener.call_args[0][0] + remove_callback = cast_listener.call_args[0][1] def discover_chromecast(service_name: str, info: ChromecastInfo) -> None: """Discover a chromecast device.""" @@ -141,7 +157,15 @@ async def async_setup_cast_internal_discovery(hass, config=None): ) discovery_callback(info.uuid, service_name) - return discover_chromecast, add_entities + def remove_chromecast(service_name: str, info: ChromecastInfo) -> None: + """Remove a chromecast device.""" + remove_callback( + info.uuid, + service_name, + (set(), info.uuid, info.model_name, info.friendly_name), + ) + + return discover_chromecast, remove_chromecast, add_entities async def async_setup_media_player_cast(hass: HomeAssistantType, info: ChromecastInfo): @@ -183,7 +207,18 @@ async def async_setup_media_player_cast(hass: HomeAssistantType, info: Chromecas await hass.async_block_till_done() await hass.async_block_till_done() assert get_chromecast.call_count == 1 - return chromecast + + def discover_chromecast(service_name: str, info: ChromecastInfo) -> None: + """Discover a chromecast device.""" + listener.services[info.uuid] = ( + {service_name}, + info.uuid, + info.model_name, + info.friendly_name, + ) + discovery_callback(info.uuid, service_name) + + return chromecast, discover_chromecast def get_status_callbacks(chromecast_mock, mz_mock=None): @@ -219,6 +254,123 @@ async def test_start_discovery_called_once(hass): assert start_discovery.call_count == 1 +async def test_internal_discovery_callback_fill_out(hass): + """Test internal discovery automatically filling out information.""" + discover_cast, _, _ = await async_setup_cast_internal_discovery(hass) + info = get_fake_chromecast_info(host="host1") + zconf = get_fake_zconf(host="host1", port=8009) + full_info = attr.evolve( + info, + model_name="google home", + friendly_name="Speaker", + uuid=FakeUUID, + manufacturer="Nabu Casa", + ) + + with patch( + "homeassistant.components.cast.helpers.dial.get_device_status", + return_value=full_info, + ), patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf, + ): + signal = MagicMock() + + async_dispatcher_connect(hass, "cast_discovered", signal) + discover_cast("the-service", info) + await hass.async_block_till_done() + + # when called with incomplete info, it should use HTTP to get missing + discover = signal.mock_calls[0][1][0] + assert discover == full_info + + +async def test_internal_discovery_callback_fill_out_default_manufacturer(hass): + """Test internal discovery automatically filling out information.""" + discover_cast, _, _ = await async_setup_cast_internal_discovery(hass) + info = get_fake_chromecast_info(host="host1") + zconf = get_fake_zconf(host="host1", port=8009) + full_info = attr.evolve( + info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID + ) + + with patch( + "homeassistant.components.cast.helpers.dial.get_device_status", + return_value=full_info, + ), patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf, + ): + signal = MagicMock() + + async_dispatcher_connect(hass, "cast_discovered", signal) + discover_cast("the-service", info) + await hass.async_block_till_done() + + # when called with incomplete info, it should use HTTP to get missing + discover = signal.mock_calls[0][1][0] + assert discover == attr.evolve(full_info, manufacturer="Google Inc.") + + +async def test_internal_discovery_callback_fill_out_fail(hass): + """Test internal discovery automatically filling out information.""" + discover_cast, _, _ = await async_setup_cast_internal_discovery(hass) + info = get_fake_chromecast_info(host="host1") + zconf = get_fake_zconf(host="host1", port=8009) + full_info = ( + info # attr.evolve(info, model_name="", friendly_name="Speaker", uuid=FakeUUID) + ) + + with patch( + "homeassistant.components.cast.helpers.dial.get_device_status", + return_value=None, + ), patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf, + ): + signal = MagicMock() + + async_dispatcher_connect(hass, "cast_discovered", signal) + discover_cast("the-service", info) + await hass.async_block_till_done() + + # when called with incomplete info, it should use HTTP to get missing + discover = signal.mock_calls[0][1][0] + assert discover == full_info + # assert 1 == 2 + + +async def test_internal_discovery_callback_fill_out_group(hass): + """Test internal discovery automatically filling out information.""" + discover_cast, _, _ = await async_setup_cast_internal_discovery(hass) + info = get_fake_chromecast_info(host="host1", port=12345) + zconf = get_fake_zconf(host="host1", port=12345) + full_info = attr.evolve( + info, + model_name="", + friendly_name="Speaker", + uuid=FakeUUID, + is_dynamic_group=False, + ) + + with patch( + "homeassistant.components.cast.helpers.dial.get_device_status", + return_value=full_info, + ), patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf, + ): + signal = MagicMock() + + async_dispatcher_connect(hass, "cast_discovered", signal) + discover_cast("the-service", info) + await hass.async_block_till_done() + + # when called with incomplete info, it should use HTTP to get missing + discover = signal.mock_calls[0][1][0] + assert discover == full_info + + async def test_stop_discovery_called_on_stop(hass): """Test pychromecast.stop_discovery called on shutdown.""" browser = MagicMock(zc={}) @@ -272,7 +424,7 @@ async def test_replay_past_chromecasts(hass): zconf_1 = get_fake_zconf(host="host1", port=8009) zconf_2 = get_fake_zconf(host="host2", port=8009) - discover_cast, add_dev1 = await async_setup_cast_internal_discovery( + discover_cast, _, add_dev1 = await async_setup_cast_internal_discovery( hass, config={"uuid": FakeUUID} ) @@ -308,7 +460,7 @@ async def test_manual_cast_chromecasts_uuid(hass): zconf_2 = get_fake_zconf(host="host_2") # Manual configuration of media player with host "configured_host" - discover_cast, add_dev1 = await async_setup_cast_internal_discovery( + discover_cast, _, add_dev1 = await async_setup_cast_internal_discovery( hass, config={"uuid": FakeUUID} ) with patch( @@ -338,7 +490,7 @@ async def test_auto_cast_chromecasts(hass): zconf_2 = get_fake_zconf(host="other_host") # Manual configuration of media player with host "configured_host" - discover_cast, add_dev1 = await async_setup_cast_internal_discovery(hass) + discover_cast, _, add_dev1 = await async_setup_cast_internal_discovery(hass) with patch( "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", return_value=zconf_1, @@ -358,6 +510,79 @@ async def test_auto_cast_chromecasts(hass): assert add_dev1.call_count == 2 +async def test_discover_dynamic_group(hass, dial_mock, pycast_mock, caplog): + """Test dynamic group does not create device or entity.""" + cast_1 = get_fake_chromecast_info(host="host_1", port=23456, uuid=FakeUUID) + cast_2 = get_fake_chromecast_info(host="host_2", port=34567, uuid=FakeUUID2) + zconf_1 = get_fake_zconf(host="host_1", port=23456) + zconf_2 = get_fake_zconf(host="host_2", port=34567) + + reg = await hass.helpers.entity_registry.async_get_registry() + + # Fake dynamic group info + tmp1 = MagicMock() + tmp1.uuid = FakeUUID + tmp2 = MagicMock() + tmp2.uuid = FakeUUID2 + dial_mock.get_multizone_status.return_value.dynamic_groups = [tmp1, tmp2] + + pycast_mock.get_chromecast_from_service.assert_not_called() + discover_cast, remove_cast, add_dev1 = await async_setup_cast_internal_discovery( + hass + ) + + # Discover cast service + with patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf_1, + ): + discover_cast("service", cast_1) + await hass.async_block_till_done() + await hass.async_block_till_done() # having tasks that add jobs + pycast_mock.get_chromecast_from_service.assert_called() + pycast_mock.get_chromecast_from_service.reset_mock() + assert add_dev1.call_count == 0 + assert reg.async_get_entity_id("media_player", "cast", cast_1.uuid) is None + + # Discover other dynamic group cast service + with patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf_2, + ): + discover_cast("service", cast_2) + await hass.async_block_till_done() + await hass.async_block_till_done() # having tasks that add jobs + pycast_mock.get_chromecast_from_service.assert_called() + pycast_mock.get_chromecast_from_service.reset_mock() + assert add_dev1.call_count == 0 + assert reg.async_get_entity_id("media_player", "cast", cast_1.uuid) is None + + # Get update for cast service + with patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf_1, + ): + discover_cast("service", cast_1) + await hass.async_block_till_done() + await hass.async_block_till_done() # having tasks that add jobs + pycast_mock.get_chromecast_from_service.assert_not_called() + assert add_dev1.call_count == 0 + assert reg.async_get_entity_id("media_player", "cast", cast_1.uuid) is None + + # Remove cast service + assert "Disconnecting from chromecast" not in caplog.text + + with patch( + "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", + return_value=zconf_1, + ): + remove_cast("service", cast_1) + await hass.async_block_till_done() + await hass.async_block_till_done() # having tasks that add jobs + + assert "Disconnecting from chromecast" in caplog.text + + async def test_update_cast_chromecasts(hass): """Test discovery of same UUID twice only adds one cast.""" cast_1 = get_fake_chromecast_info(host="old_host") @@ -366,7 +591,7 @@ async def test_update_cast_chromecasts(hass): zconf_2 = get_fake_zconf(host="new_host") # Manual configuration of media player with host "configured_host" - discover_cast, add_dev1 = await async_setup_cast_internal_discovery(hass) + discover_cast, _, add_dev1 = await async_setup_cast_internal_discovery(hass) with patch( "homeassistant.components.cast.discovery.ChromeCastZeroconf.get_zeroconf", @@ -392,7 +617,7 @@ async def test_entity_availability(hass: HomeAssistantType): entity_id = "media_player.speaker" info = get_fake_chromecast_info() - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) _, conn_status_cb, _ = get_status_callbacks(chromecast) state = hass.states.get(entity_id) @@ -423,7 +648,7 @@ async def test_entity_cast_status(hass: HomeAssistantType): info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID ) - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) cast_status_cb, conn_status_cb, _ = get_status_callbacks(chromecast) connection_status = MagicMock() @@ -466,7 +691,7 @@ async def test_entity_play_media(hass: HomeAssistantType): info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID ) - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) _, conn_status_cb, _ = get_status_callbacks(chromecast) connection_status = MagicMock() @@ -495,7 +720,7 @@ async def test_entity_play_media_cast(hass: HomeAssistantType, quick_play_mock): info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID ) - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) _, conn_status_cb, _ = get_status_callbacks(chromecast) connection_status = MagicMock() @@ -528,7 +753,7 @@ async def test_entity_play_media_cast_invalid(hass, caplog, quick_play_mock): info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID ) - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) _, conn_status_cb, _ = get_status_callbacks(chromecast) connection_status = MagicMock() @@ -575,7 +800,7 @@ async def test_entity_play_media_sign_URL(hass: HomeAssistantType): info = get_fake_chromecast_info() - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) _, conn_status_cb, _ = get_status_callbacks(chromecast) connection_status = MagicMock() @@ -601,7 +826,7 @@ async def test_entity_media_content_type(hass: HomeAssistantType): info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID ) - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) _, conn_status_cb, media_status_cb = get_status_callbacks(chromecast) connection_status = MagicMock() @@ -655,7 +880,7 @@ async def test_entity_control(hass: HomeAssistantType): info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID ) - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) _, conn_status_cb, media_status_cb = get_status_callbacks(chromecast) connection_status = MagicMock() @@ -738,7 +963,7 @@ async def test_entity_media_states(hass: HomeAssistantType): info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID ) - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) _, conn_status_cb, media_status_cb = get_status_callbacks(chromecast) connection_status = MagicMock() @@ -797,7 +1022,7 @@ async def test_group_media_states(hass, mz_mock): info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID ) - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) _, conn_status_cb, media_status_cb, group_media_status_cb = get_status_callbacks( chromecast, mz_mock ) @@ -841,7 +1066,7 @@ async def test_group_media_states(hass, mz_mock): async def test_group_media_control(hass, mz_mock): - """Test media states are read from group if entity has no state.""" + """Test media controls are handled by group if entity has no state.""" entity_id = "media_player.speaker" reg = await hass.helpers.entity_registry.async_get_registry() @@ -850,7 +1075,7 @@ async def test_group_media_control(hass, mz_mock): info, model_name="google home", friendly_name="Speaker", uuid=FakeUUID ) - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) _, conn_status_cb, media_status_cb, group_media_status_cb = get_status_callbacks( chromecast, mz_mock @@ -904,7 +1129,7 @@ async def test_group_media_control(hass, mz_mock): async def test_failed_cast_on_idle(hass, caplog): """Test no warning when unless player went idle with reason "ERROR".""" info = get_fake_chromecast_info() - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) _, _, media_status_cb = get_status_callbacks(chromecast) media_status = MagicMock(images=None) @@ -939,7 +1164,7 @@ async def test_failed_cast_other_url(hass, caplog): ) info = get_fake_chromecast_info() - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) _, _, media_status_cb = get_status_callbacks(chromecast) media_status = MagicMock(images=None) @@ -962,7 +1187,7 @@ async def test_failed_cast_internal_url(hass, caplog): ) info = get_fake_chromecast_info() - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) _, _, media_status_cb = get_status_callbacks(chromecast) media_status = MagicMock(images=None) @@ -990,7 +1215,7 @@ async def test_failed_cast_external_url(hass, caplog): ) info = get_fake_chromecast_info() - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) _, _, media_status_cb = get_status_callbacks(chromecast) media_status = MagicMock(images=None) @@ -1014,7 +1239,7 @@ async def test_failed_cast_tts_base_url(hass, caplog): ) info = get_fake_chromecast_info() - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) _, _, media_status_cb = get_status_callbacks(chromecast) media_status = MagicMock(images=None) @@ -1032,7 +1257,7 @@ async def test_disconnect_on_stop(hass: HomeAssistantType): """Test cast device disconnects socket on stop.""" info = get_fake_chromecast_info() - chromecast = await async_setup_media_player_cast(hass, info) + chromecast, _ = await async_setup_media_player_cast(hass, info) hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() diff --git a/tests/components/cert_expiry/test_config_flow.py b/tests/components/cert_expiry/test_config_flow.py index e5d90e12d13..ed51ebf70a4 100644 --- a/tests/components/cert_expiry/test_config_flow.py +++ b/tests/components/cert_expiry/test_config_flow.py @@ -1,6 +1,7 @@ """Tests for the Cert Expiry config flow.""" import socket import ssl +from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.components.cert_expiry.const import DEFAULT_PORT, DOMAIN @@ -9,7 +10,6 @@ from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from .const import HOST, PORT from .helpers import future_timestamp -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/cert_expiry/test_init.py b/tests/components/cert_expiry/test_init.py index 6aa8568a9d1..ea31ba50ea0 100644 --- a/tests/components/cert_expiry/test_init.py +++ b/tests/components/cert_expiry/test_init.py @@ -1,5 +1,6 @@ """Tests for Cert Expiry setup.""" from datetime import timedelta +from unittest.mock import patch from homeassistant.components.cert_expiry.const import DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -11,7 +12,6 @@ import homeassistant.util.dt as dt_util from .const import HOST, PORT from .helpers import future_timestamp, static_datetime -from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/cert_expiry/test_sensors.py b/tests/components/cert_expiry/test_sensors.py index 4a78f02b39c..375b676eaf8 100644 --- a/tests/components/cert_expiry/test_sensors.py +++ b/tests/components/cert_expiry/test_sensors.py @@ -2,6 +2,7 @@ from datetime import timedelta import socket import ssl +from unittest.mock import patch from homeassistant.components.cert_expiry.const import DOMAIN from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY @@ -11,7 +12,6 @@ from homeassistant.util.dt import utcnow from .const import HOST, PORT from .helpers import future_timestamp, static_datetime -from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/climate/test_init.py b/tests/components/climate/test_init.py index 89c12b4c517..8113c1e343a 100644 --- a/tests/components/climate/test_init.py +++ b/tests/components/climate/test_init.py @@ -1,5 +1,6 @@ """The tests for the climate component.""" from typing import List +from unittest.mock import MagicMock import pytest import voluptuous as vol @@ -12,7 +13,6 @@ from homeassistant.components.climate import ( ClimateEntity, ) -from tests.async_mock import MagicMock from tests.common import async_mock_service diff --git a/tests/components/cloud/__init__.py b/tests/components/cloud/__init__.py index da7c6ff13d0..8613c6408fe 100644 --- a/tests/components/cloud/__init__.py +++ b/tests/components/cloud/__init__.py @@ -1,11 +1,11 @@ """Tests for the cloud component.""" +from unittest.mock import AsyncMock, patch + from homeassistant.components import cloud from homeassistant.components.cloud import const from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, patch - async def mock_cloud(hass, config=None): """Mock cloud.""" diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index 02d9b4c41aa..4755d470418 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -1,4 +1,6 @@ """Fixtures for cloud tests.""" +from unittest.mock import patch + import jwt import pytest @@ -6,8 +8,6 @@ from homeassistant.components.cloud import const, prefs from . import mock_cloud, mock_cloud_prefs -from tests.async_mock import patch - @pytest.fixture(autouse=True) def mock_user_data(): diff --git a/tests/components/cloud/test_account_link.py b/tests/components/cloud/test_account_link.py index 1580969b0a5..62225597939 100644 --- a/tests/components/cloud/test_account_link.py +++ b/tests/components/cloud/test_account_link.py @@ -2,6 +2,7 @@ import asyncio import logging from time import time +from unittest.mock import AsyncMock, Mock, patch import pytest @@ -10,7 +11,6 @@ from homeassistant.components.cloud import account_link from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.util.dt import utcnow -from tests.async_mock import AsyncMock, Mock, patch from tests.common import async_fire_time_changed, mock_platform TEST_DOMAIN = "oauth2_test" diff --git a/tests/components/cloud/test_alexa_config.py b/tests/components/cloud/test_alexa_config.py index ce761952921..966ef4b0af3 100644 --- a/tests/components/cloud/test_alexa_config.py +++ b/tests/components/cloud/test_alexa_config.py @@ -1,11 +1,11 @@ """Test Alexa config.""" import contextlib +from unittest.mock import AsyncMock, Mock, patch from homeassistant.components.cloud import ALEXA_SCHEMA, alexa_config from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED from homeassistant.util.dt import utcnow -from tests.async_mock import AsyncMock, Mock, patch from tests.common import async_fire_time_changed diff --git a/tests/components/cloud/test_binary_sensor.py b/tests/components/cloud/test_binary_sensor.py index 6a2d76dc403..c9c9d53981e 100644 --- a/tests/components/cloud/test_binary_sensor.py +++ b/tests/components/cloud/test_binary_sensor.py @@ -1,9 +1,9 @@ """Tests for the cloud binary sensor.""" +from unittest.mock import Mock, patch + from homeassistant.components.cloud.const import DISPATCHER_REMOTE_UPDATE from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch - async def test_remote_connection_sensor(hass): """Test the remote connection sensor.""" diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index 752394b5d0f..dfea8f80cee 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -1,5 +1,6 @@ """Test the cloud.iot module.""" from datetime import timedelta +from unittest.mock import AsyncMock, MagicMock, Mock, patch import aiohttp from aiohttp import web @@ -15,7 +16,6 @@ from homeassistant.util import dt as dt_util from . import mock_cloud, mock_cloud_prefs -from tests.async_mock import AsyncMock, MagicMock, Mock, patch from tests.common import async_fire_time_changed from tests.components.alexa import test_smart_home as test_alexa diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 78207605830..f58ea1a415b 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -1,4 +1,6 @@ """Test the Cloud Google Config.""" +from unittest.mock import AsyncMock, Mock, patch + import pytest from homeassistant.components.cloud import GACTIONS_SCHEMA @@ -9,7 +11,6 @@ from homeassistant.core import CoreState, State from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED from homeassistant.util.dt import utcnow -from tests.async_mock import AsyncMock, Mock, patch from tests.common import async_fire_time_changed diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index a3c33b31ebb..80641c304be 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -1,9 +1,10 @@ """Tests for the HTTP API for the cloud component.""" import asyncio from ipaddress import ip_network +from unittest.mock import AsyncMock, MagicMock, Mock, patch import aiohttp -from hass_nabucasa import thingtalk +from hass_nabucasa import thingtalk, voice from hass_nabucasa.auth import Unauthenticated, UnknownError from hass_nabucasa.const import STATE_CONNECTED from jose import jwt @@ -19,7 +20,6 @@ from homeassistant.core import State from . import mock_cloud, mock_cloud_prefs -from tests.async_mock import AsyncMock, MagicMock, Mock, patch from tests.components.google_assistant import MockConfig SUBSCRIPTION_INFO_URL = "https://api-test.hass.io/subscription_info" @@ -361,6 +361,7 @@ async def test_websocket_status( "alexa_report_state": False, "google_report_state": False, "remote_enabled": False, + "tts_default_voice": ["en-US", "female"], }, "alexa_entities": { "include_domains": [], @@ -491,6 +492,7 @@ async def test_websocket_update_preferences( "google_secure_devices_pin": "1234", "google_default_expose": ["light", "switch"], "alexa_default_expose": ["sensor", "media_player"], + "tts_default_voice": ["en-GB", "male"], } ) response = await client.receive_json() @@ -501,6 +503,7 @@ async def test_websocket_update_preferences( assert setup_api.google_secure_devices_pin == "1234" assert setup_api.google_default_expose == ["light", "switch"] assert setup_api.alexa_default_expose == ["sensor", "media_player"] + assert setup_api.tts_default_voice == ("en-GB", "male") async def test_websocket_update_preferences_require_relink( @@ -975,3 +978,25 @@ async def test_thingtalk_convert_internal(hass, hass_ws_client, setup_api): assert not response["success"] assert response["error"]["code"] == "unknown_error" assert response["error"]["message"] == "Did not understand" + + +async def test_tts_info(hass, hass_ws_client, setup_api): + """Test that we can get TTS info.""" + # Verify the format is as expected + assert voice.MAP_VOICE[("en-US", voice.Gender.FEMALE)] == "JennyNeural" + + client = await hass_ws_client(hass) + + with patch.dict( + "homeassistant.components.cloud.http_api.MAP_VOICE", + { + ("en-US", voice.Gender.MALE): "GuyNeural", + ("en-US", voice.Gender.FEMALE): "JennyNeural", + }, + clear=True, + ): + await client.send_json({"id": 5, "type": "cloud/tts/info"}) + response = await client.receive_json() + + assert response["success"] + assert response["result"] == {"languages": [["en-US", "male"], ["en-US", "female"]]} diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index e174b080102..7202c8a0b39 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -1,5 +1,7 @@ """Test the cloud component.""" +from unittest.mock import patch + import pytest from homeassistant.components import cloud @@ -10,8 +12,6 @@ from homeassistant.core import Context from homeassistant.exceptions import Unauthorized from homeassistant.setup import async_setup_component -from tests.async_mock import patch - async def test_constructor_loads_info_from_config(hass): """Test non-dev mode loads info from SERVERS constant.""" diff --git a/tests/components/cloud/test_prefs.py b/tests/components/cloud/test_prefs.py index 4f2d5d6d661..d1b6f9ed867 100644 --- a/tests/components/cloud/test_prefs.py +++ b/tests/components/cloud/test_prefs.py @@ -1,9 +1,9 @@ """Test Cloud preferences.""" +from unittest.mock import patch + from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components.cloud.prefs import STORAGE_KEY, CloudPreferences -from tests.async_mock import patch - async def test_set_username(hass): """Test we clear config if we set different username.""" diff --git a/tests/components/cloud/test_system_health.py b/tests/components/cloud/test_system_health.py index b69ab462ddb..65ffd859f33 100644 --- a/tests/components/cloud/test_system_health.py +++ b/tests/components/cloud/test_system_health.py @@ -1,12 +1,12 @@ """Test cloud system health.""" import asyncio +from unittest.mock import Mock from aiohttp import ClientError from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from tests.async_mock import Mock from tests.common import get_system_health_info diff --git a/tests/components/cloud/test_tts.py b/tests/components/cloud/test_tts.py index 32a4ca7cb50..23760956935 100644 --- a/tests/components/cloud/test_tts.py +++ b/tests/components/cloud/test_tts.py @@ -1,5 +1,22 @@ """Tests for cloud tts.""" -from homeassistant.components.cloud import tts +from unittest.mock import Mock + +from hass_nabucasa import voice +import pytest +import voluptuous as vol + +from homeassistant.components.cloud import const, tts + + +@pytest.fixture() +def cloud_with_prefs(cloud_prefs): + """Return a cloud mock with prefs.""" + return Mock(client=Mock(prefs=cloud_prefs)) + + +def test_default_exists(): + """Test our default language exists.""" + assert const.DEFAULT_TTS_DEFAULT_VOICE in voice.MAP_VOICE def test_schema(): @@ -9,7 +26,61 @@ def test_schema(): processed = tts.PLATFORM_SCHEMA({"platform": "cloud", "language": "nl-NL"}) assert processed["gender"] == "female" + with pytest.raises(vol.Invalid): + tts.PLATFORM_SCHEMA( + {"platform": "cloud", "language": "non-existing", "gender": "female"} + ) + + with pytest.raises(vol.Invalid): + tts.PLATFORM_SCHEMA( + {"platform": "cloud", "language": "nl-NL", "gender": "not-supported"} + ) + # Should not raise - processed = tts.PLATFORM_SCHEMA( - {"platform": "cloud", "language": "nl-NL", "gender": "female"} + tts.PLATFORM_SCHEMA({"platform": "cloud", "language": "nl-NL", "gender": "female"}) + tts.PLATFORM_SCHEMA({"platform": "cloud"}) + + +async def test_prefs_default_voice(hass, cloud_with_prefs, cloud_prefs): + """Test cloud provider uses the preferences.""" + assert cloud_prefs.tts_default_voice == ("en-US", "female") + + provider_pref = await tts.async_get_engine( + Mock(data={const.DOMAIN: cloud_with_prefs}), None, {} ) + provider_conf = await tts.async_get_engine( + Mock(data={const.DOMAIN: cloud_with_prefs}), + {"language": "fr-FR", "gender": "female"}, + None, + ) + + assert provider_pref.default_language == "en-US" + assert provider_pref.default_options == {"gender": "female"} + assert provider_conf.default_language == "fr-FR" + assert provider_conf.default_options == {"gender": "female"} + + await cloud_prefs.async_update(tts_default_voice=("nl-NL", "male")) + await hass.async_block_till_done() + + assert provider_pref.default_language == "nl-NL" + assert provider_pref.default_options == {"gender": "male"} + assert provider_conf.default_language == "fr-FR" + assert provider_conf.default_options == {"gender": "female"} + + +async def test_provider_properties(cloud_with_prefs): + """Test cloud provider.""" + provider = await tts.async_get_engine( + Mock(data={const.DOMAIN: cloud_with_prefs}), None, {} + ) + assert provider.supported_options == ["gender"] + assert "nl-NL" in provider.supported_languages + + +async def test_get_tts_audio(cloud_with_prefs): + """Test cloud provider.""" + provider = await tts.async_get_engine( + Mock(data={const.DOMAIN: cloud_with_prefs}), None, {} + ) + assert provider.supported_options == ["gender"] + assert "nl-NL" in provider.supported_languages diff --git a/tests/components/cloudflare/__init__.py b/tests/components/cloudflare/__init__.py index 68eed3afd87..c72a9cd84b0 100644 --- a/tests/components/cloudflare/__init__.py +++ b/tests/components/cloudflare/__init__.py @@ -1,12 +1,12 @@ """Tests for the Cloudflare integration.""" from typing import List +from unittest.mock import AsyncMock, patch from pycfdns import CFRecord from homeassistant.components.cloudflare.const import CONF_RECORDS, DOMAIN from homeassistant.const import CONF_API_TOKEN, CONF_ZONE -from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry ENTRY_CONFIG = { diff --git a/tests/components/cloudflare/conftest.py b/tests/components/cloudflare/conftest.py index 1ee381de104..99ca7af26f4 100644 --- a/tests/components/cloudflare/conftest.py +++ b/tests/components/cloudflare/conftest.py @@ -1,10 +1,10 @@ """Define fixtures available for all tests.""" +from unittest.mock import patch + from pytest import fixture from . import _get_mock_cfupdate -from tests.async_mock import patch - @fixture def cfupdate(hass): diff --git a/tests/components/coinmarketcap/test_sensor.py b/tests/components/coinmarketcap/test_sensor.py index 30fc0f0e1f7..369a006f568 100644 --- a/tests/components/coinmarketcap/test_sensor.py +++ b/tests/components/coinmarketcap/test_sensor.py @@ -1,12 +1,12 @@ """Tests for the CoinMarketCap sensor platform.""" import json +from unittest.mock import patch import pytest from homeassistant.components.sensor import DOMAIN from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import assert_setup_component, load_fixture VALID_CONFIG = { diff --git a/tests/components/color_extractor/test_service.py b/tests/components/color_extractor/test_service.py index 31b623f1c76..2a1592fe73f 100644 --- a/tests/components/color_extractor/test_service.py +++ b/tests/components/color_extractor/test_service.py @@ -1,6 +1,7 @@ """Tests for color_extractor component service calls.""" import base64 import io +from unittest.mock import Mock, mock_open, patch import aiohttp import pytest @@ -23,7 +24,6 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component import homeassistant.util.color as color_util -from tests.async_mock import Mock, mock_open, patch from tests.common import load_fixture LIGHT_ENTITY = "light.kitchen_lights" diff --git a/tests/components/comfoconnect/__init__.py b/tests/components/comfoconnect/__init__.py new file mode 100644 index 00000000000..8da47b510be --- /dev/null +++ b/tests/components/comfoconnect/__init__.py @@ -0,0 +1 @@ +"""Tests for the comfoconnect component.""" diff --git a/tests/components/comfoconnect/test_sensor.py b/tests/components/comfoconnect/test_sensor.py new file mode 100644 index 00000000000..3ae078cccef --- /dev/null +++ b/tests/components/comfoconnect/test_sensor.py @@ -0,0 +1,93 @@ +"""Tests for the comfoconnect sensor platform.""" +# import json +from unittest.mock import patch + +import pytest + +from homeassistant.components.sensor import DOMAIN +from homeassistant.setup import async_setup_component + +from tests.common import assert_setup_component + +COMPONENT = "comfoconnect" +VALID_CONFIG = { + COMPONENT: {"host": "1.2.3.4"}, + DOMAIN: { + "platform": COMPONENT, + "resources": [ + "current_humidity", + "current_temperature", + "supply_fan_duty", + "power_usage", + "preheater_power_total", + ], + }, +} + + +@pytest.fixture +def mock_bridge_discover(): + """Mock the bridge discover method.""" + with patch("pycomfoconnect.bridge.Bridge.discover") as mock_bridge_discover: + mock_bridge_discover.return_value[0].uuid.hex.return_value = "00" + yield mock_bridge_discover + + +@pytest.fixture +def mock_comfoconnect_command(): + """Mock the ComfoConnect connect method.""" + with patch( + "pycomfoconnect.comfoconnect.ComfoConnect._command" + ) as mock_comfoconnect_command: + yield mock_comfoconnect_command + + +@pytest.fixture +async def setup_sensor(hass, mock_bridge_discover, mock_comfoconnect_command): + """Set up demo sensor component.""" + with assert_setup_component(1, DOMAIN): + await async_setup_component(hass, DOMAIN, VALID_CONFIG) + await hass.async_block_till_done() + + +async def test_sensors(hass, setup_sensor): + """Test the sensors.""" + state = hass.states.get("sensor.comfoairq_inside_humidity") + assert state is not None + + assert state.name == "ComfoAirQ Inside Humidity" + assert state.attributes.get("unit_of_measurement") == "%" + assert state.attributes.get("device_class") == "humidity" + assert state.attributes.get("icon") is None + + state = hass.states.get("sensor.comfoairq_inside_temperature") + assert state is not None + + assert state.name == "ComfoAirQ Inside Temperature" + assert state.attributes.get("unit_of_measurement") == "°C" + assert state.attributes.get("device_class") == "temperature" + assert state.attributes.get("icon") is None + + state = hass.states.get("sensor.comfoairq_supply_fan_duty") + assert state is not None + + assert state.name == "ComfoAirQ Supply Fan Duty" + assert state.attributes.get("unit_of_measurement") == "%" + assert state.attributes.get("device_class") is None + assert state.attributes.get("icon") == "mdi:fan" + + state = hass.states.get("sensor.comfoairq_power_usage") + assert state is not None + + assert state.name == "ComfoAirQ Power usage" + assert state.attributes.get("unit_of_measurement") == "W" + assert state.attributes.get("device_class") == "power" + assert state.attributes.get("icon") is None + + state = hass.states.get("sensor.comfoairq_preheater_power_total") + assert state is not None + + assert state.name == "ComfoAirQ Preheater power total" + assert state.attributes.get("unit_of_measurement") == "kWh" + assert state.attributes.get("device_class") == "energy" + assert state.attributes.get("icon") is None diff --git a/tests/components/command_line/test_cover.py b/tests/components/command_line/test_cover.py index 508b1e1fb41..ee692413bcd 100644 --- a/tests/components/command_line/test_cover.py +++ b/tests/components/command_line/test_cover.py @@ -3,6 +3,7 @@ import os from os import path import tempfile from unittest import mock +from unittest.mock import patch import pytest @@ -18,8 +19,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import patch - @pytest.fixture def rs(hass): diff --git a/tests/components/command_line/test_notify.py b/tests/components/command_line/test_notify.py index 8509bc785da..3dcb521cfd2 100644 --- a/tests/components/command_line/test_notify.py +++ b/tests/components/command_line/test_notify.py @@ -2,11 +2,11 @@ import os import tempfile import unittest +from unittest.mock import patch import homeassistant.components.notify as notify from homeassistant.setup import async_setup_component, setup_component -from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py index 0a6e29ca00c..042c9acf432 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -1,10 +1,10 @@ """The tests for the Command line sensor platform.""" import unittest +from unittest.mock import patch from homeassistant.components.command_line import sensor as command_line from homeassistant.helpers.template import Template -from tests.async_mock import patch from tests.common import get_test_home_assistant diff --git a/tests/components/config/test_automation.py b/tests/components/config/test_automation.py index 347ac96f892..00c89edeef0 100644 --- a/tests/components/config/test_automation.py +++ b/tests/components/config/test_automation.py @@ -1,10 +1,10 @@ """Test Automation config panel.""" import json +from unittest.mock import patch from homeassistant.bootstrap import async_setup_component from homeassistant.components import config -from tests.async_mock import patch from tests.components.blueprint.conftest import stub_blueprint_populate # noqa diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 11299f4108b..87b1559a21b 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -1,6 +1,7 @@ """Test config entries API.""" from collections import OrderedDict +from unittest.mock import AsyncMock, patch import pytest import voluptuous as vol @@ -12,7 +13,6 @@ from homeassistant.core import callback from homeassistant.generated import config_flows from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, patch from tests.common import ( MockConfigEntry, MockModule, @@ -750,6 +750,7 @@ async def test_ignore_flow(hass, hass_ws_client): "id": 5, "type": "config_entries/ignore_flow", "flow_id": result["flow_id"], + "title": "Test Integration", } ) response = await ws_client.receive_json() @@ -761,3 +762,4 @@ async def test_ignore_flow(hass, hass_ws_client): entry = hass.config_entries.async_entries("test")[0] assert entry.source == "ignore" assert entry.unique_id == "mock-unique-id" + assert entry.title == "Test Integration" diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index 72e655dbb66..361fceab565 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -1,4 +1,6 @@ """Test hassbian config.""" +from unittest.mock import patch + import pytest from homeassistant.bootstrap import async_setup_component @@ -7,8 +9,6 @@ from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.const import CONF_UNIT_SYSTEM, CONF_UNIT_SYSTEM_IMPERIAL from homeassistant.util import dt as dt_util, location -from tests.async_mock import patch - ORIG_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE diff --git a/tests/components/config/test_customize.py b/tests/components/config/test_customize.py index ca3bcc98c7d..aac18bc379e 100644 --- a/tests/components/config/test_customize.py +++ b/tests/components/config/test_customize.py @@ -1,12 +1,11 @@ """Test Customize config panel.""" import json +from unittest.mock import patch from homeassistant.bootstrap import async_setup_component from homeassistant.components import config from homeassistant.config import DATA_CUSTOMIZE -from tests.async_mock import patch - async def test_get_entity(hass, hass_client): """Test getting entity.""" diff --git a/tests/components/config/test_group.py b/tests/components/config/test_group.py index 98ad2041713..c4b7cf25800 100644 --- a/tests/components/config/test_group.py +++ b/tests/components/config/test_group.py @@ -1,11 +1,10 @@ """Test Group config panel.""" import json +from unittest.mock import AsyncMock, patch from homeassistant.bootstrap import async_setup_component from homeassistant.components import config -from tests.async_mock import AsyncMock, patch - VIEW_NAME = "api:config:group:config" diff --git a/tests/components/config/test_init.py b/tests/components/config/test_init.py index 6dd16fef7ec..dd3e294bac3 100644 --- a/tests/components/config/test_init.py +++ b/tests/components/config/test_init.py @@ -1,10 +1,11 @@ """Test config init.""" +from unittest.mock import patch + from homeassistant.components import config from homeassistant.const import EVENT_COMPONENT_LOADED from homeassistant.setup import ATTR_COMPONENT, async_setup_component -from tests.async_mock import patch from tests.common import mock_component diff --git a/tests/components/config/test_scene.py b/tests/components/config/test_scene.py index dcaa950f342..bdb2a2e3f10 100644 --- a/tests/components/config/test_scene.py +++ b/tests/components/config/test_scene.py @@ -1,12 +1,11 @@ """Test Automation config panel.""" import json +from unittest.mock import patch from homeassistant.bootstrap import async_setup_component from homeassistant.components import config from homeassistant.util.yaml import dump -from tests.async_mock import patch - async def test_update_scene(hass, hass_client): """Test updating a scene.""" diff --git a/tests/components/config/test_script.py b/tests/components/config/test_script.py index 4dc906e92f3..0026729766c 100644 --- a/tests/components/config/test_script.py +++ b/tests/components/config/test_script.py @@ -1,9 +1,9 @@ """Tests for config/script.""" +from unittest.mock import patch + from homeassistant.bootstrap import async_setup_component from homeassistant.components import config -from tests.async_mock import patch - async def test_delete_script(hass, hass_client): """Test deleting a script.""" diff --git a/tests/components/config/test_zwave.py b/tests/components/config/test_zwave.py index 8b83583d1f5..2f15a167c92 100644 --- a/tests/components/config/test_zwave.py +++ b/tests/components/config/test_zwave.py @@ -1,5 +1,6 @@ """Test Z-Wave config panel.""" import json +from unittest.mock import MagicMock, patch import pytest @@ -8,7 +9,6 @@ from homeassistant.components import config from homeassistant.components.zwave import DATA_NETWORK, const from homeassistant.const import HTTP_NOT_FOUND -from tests.async_mock import MagicMock, patch from tests.mock.zwave import MockEntityValues, MockNode, MockValue VIEW_NAME = "api:config:zwave:device_config" diff --git a/tests/components/conftest.py b/tests/components/conftest.py index acfbdeb8629..3b1781ba510 100644 --- a/tests/components/conftest.py +++ b/tests/components/conftest.py @@ -1,10 +1,10 @@ """Fixtures for component testing.""" +from unittest.mock import patch + import pytest from homeassistant.components import zeroconf -from tests.async_mock import patch - zeroconf.orig_install_multiple_zeroconf_catcher = ( zeroconf.install_multiple_zeroconf_catcher ) diff --git a/tests/components/control4/test_config_flow.py b/tests/components/control4/test_config_flow.py index 8c68039920d..48ff8201166 100644 --- a/tests/components/control4/test_config_flow.py +++ b/tests/components/control4/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Control4 config flow.""" import datetime +from unittest.mock import AsyncMock, patch from pyControl4.account import C4Account from pyControl4.director import C4Director @@ -14,7 +15,6 @@ from homeassistant.const import ( CONF_USERNAME, ) -from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry diff --git a/tests/components/coolmaster/test_config_flow.py b/tests/components/coolmaster/test_config_flow.py index 27e44949585..3dd0f27ecdf 100644 --- a/tests/components/coolmaster/test_config_flow.py +++ b/tests/components/coolmaster/test_config_flow.py @@ -1,9 +1,9 @@ """Test the Coolmaster config flow.""" +from unittest.mock import patch + from homeassistant import config_entries from homeassistant.components.coolmaster.const import AVAILABLE_MODES, DOMAIN -from tests.async_mock import patch - def _flow_data(): options = {"host": "1.1.1.1"} diff --git a/tests/components/coronavirus/conftest.py b/tests/components/coronavirus/conftest.py index bbe5a463802..57128268fd7 100644 --- a/tests/components/coronavirus/conftest.py +++ b/tests/components/coronavirus/conftest.py @@ -1,8 +1,8 @@ """Test helpers.""" -import pytest +from unittest.mock import Mock, patch -from tests.async_mock import Mock, patch +import pytest @pytest.fixture(autouse=True) diff --git a/tests/components/daikin/test_config_flow.py b/tests/components/daikin/test_config_flow.py index 3e4ddb6d3fd..a7165b2cb9b 100644 --- a/tests/components/daikin/test_config_flow.py +++ b/tests/components/daikin/test_config_flow.py @@ -1,6 +1,7 @@ # pylint: disable=redefined-outer-name """Tests for the Daikin config flow.""" import asyncio +from unittest.mock import PropertyMock, patch from aiohttp import ClientError from aiohttp.web_exceptions import HTTPForbidden @@ -20,7 +21,6 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) -from tests.async_mock import PropertyMock, patch from tests.common import MockConfigEntry MAC = "AABBCCDDEEFF" diff --git a/tests/components/darksky/test_sensor.py b/tests/components/darksky/test_sensor.py index be1e9849452..02bc392cd68 100644 --- a/tests/components/darksky/test_sensor.py +++ b/tests/components/darksky/test_sensor.py @@ -2,6 +2,7 @@ from datetime import timedelta import re import unittest +from unittest.mock import MagicMock, patch import forecastio from requests.exceptions import HTTPError @@ -10,7 +11,6 @@ import requests_mock from homeassistant.components.darksky import sensor as darksky from homeassistant.setup import setup_component -from tests.async_mock import MagicMock, patch from tests.common import get_test_home_assistant, load_fixture VALID_CONFIG_MINIMAL = { diff --git a/tests/components/darksky/test_weather.py b/tests/components/darksky/test_weather.py index 1a2a2e156d9..9a1b3912b87 100644 --- a/tests/components/darksky/test_weather.py +++ b/tests/components/darksky/test_weather.py @@ -1,6 +1,7 @@ """The tests for the Dark Sky weather component.""" import re import unittest +from unittest.mock import patch import forecastio from requests.exceptions import ConnectionError @@ -10,7 +11,6 @@ from homeassistant.components import weather from homeassistant.setup import setup_component from homeassistant.util.unit_system import METRIC_SYSTEM -from tests.async_mock import patch from tests.common import get_test_home_assistant, load_fixture diff --git a/tests/components/datadog/test_init.py b/tests/components/datadog/test_init.py index f46ad027442..087e0f4b884 100644 --- a/tests/components/datadog/test_init.py +++ b/tests/components/datadog/test_init.py @@ -1,5 +1,6 @@ """The tests for the Datadog component.""" from unittest import mock +from unittest.mock import MagicMock, patch import homeassistant.components.datadog as datadog from homeassistant.const import ( @@ -11,7 +12,6 @@ from homeassistant.const import ( import homeassistant.core as ha from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, patch from tests.common import assert_setup_component diff --git a/tests/components/debugpy/test_init.py b/tests/components/debugpy/test_init.py index 86be0d788f6..97d08e13bfc 100644 --- a/tests/components/debugpy/test_init.py +++ b/tests/components/debugpy/test_init.py @@ -1,4 +1,6 @@ """Tests for the Remote Python Debugger integration.""" +from unittest.mock import patch + import pytest from homeassistant.components.debugpy import ( @@ -12,8 +14,6 @@ from homeassistant.components.debugpy import ( from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.async_mock import patch - @pytest.fixture def mock_debugpy(): diff --git a/tests/components/deconz/test_binary_sensor.py b/tests/components/deconz/test_binary_sensor.py index 78a4f1e937d..3611e30f665 100644 --- a/tests/components/deconz/test_binary_sensor.py +++ b/tests/components/deconz/test_binary_sensor.py @@ -1,6 +1,7 @@ """deCONZ binary sensor platform tests.""" from copy import deepcopy +from unittest.mock import patch from homeassistant.components.binary_sensor import ( DEVICE_CLASS_MOTION, @@ -21,8 +22,6 @@ from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration -from tests.async_mock import patch - SENSORS = { "1": { "id": "Presence sensor id", diff --git a/tests/components/deconz/test_climate.py b/tests/components/deconz/test_climate.py index 319675cf6f7..4d68ba2a6a7 100644 --- a/tests/components/deconz/test_climate.py +++ b/tests/components/deconz/test_climate.py @@ -1,6 +1,7 @@ """deCONZ climate platform tests.""" from copy import deepcopy +from unittest.mock import patch import pytest @@ -43,8 +44,6 @@ from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration -from tests.async_mock import patch - SENSORS = { "1": { "id": "Thermostat id", diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index d922dffb623..e18418ff9ae 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -1,6 +1,7 @@ """Tests for deCONZ config flow.""" import asyncio +from unittest.mock import patch import pydeconz @@ -14,14 +15,19 @@ from homeassistant.components.deconz.const import ( CONF_ALLOW_DECONZ_GROUPS, CONF_ALLOW_NEW_DEVICES, CONF_MASTER_GATEWAY, - DOMAIN, + DOMAIN as DECONZ_DOMAIN, ) from homeassistant.components.ssdp import ( ATTR_SSDP_LOCATION, ATTR_UPNP_MANUFACTURER_URL, ATTR_UPNP_SERIAL, ) -from homeassistant.config_entries import SOURCE_HASSIO, SOURCE_SSDP, SOURCE_USER +from homeassistant.config_entries import ( + SOURCE_HASSIO, + SOURCE_REAUTH, + SOURCE_SSDP, + SOURCE_USER, +) from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, CONTENT_TYPE_JSON from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -31,8 +37,6 @@ from homeassistant.data_entry_flow import ( from .test_gateway import API_KEY, BRIDGEID, setup_deconz_integration -from tests.async_mock import patch - BAD_BRIDGEID = "0000000000000000" @@ -48,7 +52,7 @@ async def test_flow_discovered_bridges(hass, aioclient_mock): ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DECONZ_DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == RESULT_TYPE_FORM @@ -89,7 +93,7 @@ async def test_flow_manual_configuration_decision(hass, aioclient_mock): ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DECONZ_DOMAIN, context={"source": SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( @@ -141,7 +145,7 @@ async def test_flow_manual_configuration(hass, aioclient_mock): ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DECONZ_DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == RESULT_TYPE_FORM @@ -185,7 +189,7 @@ async def test_manual_configuration_after_discovery_timeout(hass, aioclient_mock aioclient_mock.get(pydeconz.utils.URL_DISCOVER, exc=asyncio.TimeoutError) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DECONZ_DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == RESULT_TYPE_FORM @@ -198,7 +202,7 @@ async def test_manual_configuration_after_discovery_ResponseError(hass, aioclien aioclient_mock.get(pydeconz.utils.URL_DISCOVER, exc=pydeconz.errors.ResponseError) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DECONZ_DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == RESULT_TYPE_FORM @@ -217,7 +221,7 @@ async def test_manual_configuration_update_configuration(hass, aioclient_mock): ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DECONZ_DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == RESULT_TYPE_FORM @@ -263,7 +267,7 @@ async def test_manual_configuration_dont_update_configuration(hass, aioclient_mo ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DECONZ_DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == RESULT_TYPE_FORM @@ -306,7 +310,7 @@ async def test_manual_configuration_timeout_get_bridge(hass, aioclient_mock): ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DECONZ_DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == RESULT_TYPE_FORM @@ -347,7 +351,7 @@ async def test_link_get_api_key_ResponseError(hass, aioclient_mock): ) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER} + DECONZ_DOMAIN, context={"source": SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( @@ -368,10 +372,46 @@ async def test_link_get_api_key_ResponseError(hass, aioclient_mock): assert result["errors"] == {"base": "no_key"} +async def test_reauth_flow_update_configuration(hass, aioclient_mock): + """Verify reauth flow can update gateway API key.""" + config_entry = await setup_deconz_integration(hass) + + result = await hass.config_entries.flow.async_init( + DECONZ_DOMAIN, + data=config_entry.data, + context={"source": SOURCE_REAUTH}, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "link" + + new_api_key = "new_key" + + aioclient_mock.post( + "http://1.2.3.4:80/api", + json=[{"success": {"username": new_api_key}}], + headers={"content-type": CONTENT_TYPE_JSON}, + ) + + aioclient_mock.get( + f"http://1.2.3.4:80/api/{new_api_key}/config", + json={"bridgeid": BRIDGEID}, + headers={"content-type": CONTENT_TYPE_JSON}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert config_entry.data[CONF_API_KEY] == new_api_key + + async def test_flow_ssdp_discovery(hass, aioclient_mock): """Test that config flow for one discovered bridge works.""" result = await hass.config_entries.flow.async_init( - DOMAIN, + DECONZ_DOMAIN, data={ ATTR_SSDP_LOCATION: "http://1.2.3.4:80/", ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL, @@ -411,7 +451,7 @@ async def test_ssdp_discovery_update_configuration(hass): return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( - DOMAIN, + DECONZ_DOMAIN, data={ ATTR_SSDP_LOCATION: "http://2.3.4.5:80/", ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL, @@ -432,7 +472,7 @@ async def test_ssdp_discovery_dont_update_configuration(hass): config_entry = await setup_deconz_integration(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, + DECONZ_DOMAIN, data={ ATTR_SSDP_LOCATION: "http://1.2.3.4:80/", ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL, @@ -451,7 +491,7 @@ async def test_ssdp_discovery_dont_update_existing_hassio_configuration(hass): config_entry = await setup_deconz_integration(hass, source=SOURCE_HASSIO) result = await hass.config_entries.flow.async_init( - DOMAIN, + DECONZ_DOMAIN, data={ ATTR_SSDP_LOCATION: "http://1.2.3.4:80/", ATTR_UPNP_MANUFACTURER_URL: DECONZ_MANUFACTURERURL, @@ -468,7 +508,7 @@ async def test_ssdp_discovery_dont_update_existing_hassio_configuration(hass): async def test_flow_hassio_discovery(hass): """Test hassio discovery flow works.""" result = await hass.config_entries.flow.async_init( - DOMAIN, + DECONZ_DOMAIN, data={ "addon": "Mock Addon", CONF_HOST: "mock-deconz", @@ -512,7 +552,7 @@ async def test_hassio_discovery_update_configuration(hass): return_value=True, ) as mock_setup_entry: result = await hass.config_entries.flow.async_init( - DOMAIN, + DECONZ_DOMAIN, data={ CONF_HOST: "2.3.4.5", CONF_PORT: 8080, @@ -536,7 +576,7 @@ async def test_hassio_discovery_dont_update_configuration(hass): await setup_deconz_integration(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, + DECONZ_DOMAIN, data={ CONF_HOST: "1.2.3.4", CONF_PORT: 80, diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index 374d3683a6e..5314a41b315 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -1,6 +1,7 @@ """deCONZ cover platform tests.""" from copy import deepcopy +from unittest.mock import patch from homeassistant.components.cover import ( ATTR_CURRENT_TILT_POSITION, @@ -23,8 +24,6 @@ from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration -from tests.async_mock import patch - COVERS = { "1": { "id": "Level controllable cover id", diff --git a/tests/components/deconz/test_fan.py b/tests/components/deconz/test_fan.py index 6806071dd75..b9c154a2791 100644 --- a/tests/components/deconz/test_fan.py +++ b/tests/components/deconz/test_fan.py @@ -1,6 +1,7 @@ """deCONZ fan platform tests.""" from copy import deepcopy +from unittest.mock import patch import pytest @@ -22,8 +23,6 @@ from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration -from tests.async_mock import patch - FANS = { "1": { "etag": "432f3de28965052961a99e3c5494daf4", diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index 12666cdb692..1790b6ed6e1 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -1,6 +1,7 @@ """Test deCONZ gateway.""" from copy import deepcopy +from unittest.mock import Mock, patch import pydeconz import pytest @@ -31,7 +32,6 @@ from homeassistant.config_entries import CONN_CLASS_LOCAL_PUSH, SOURCE_SSDP from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.helpers.dispatcher import async_dispatcher_connect -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry API_KEY = "1234567890ABCDEF" @@ -181,6 +181,18 @@ async def test_update_address(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_gateway_trigger_reauth_flow(hass): + """Failed authentication trigger a reauthentication flow.""" + with patch( + "homeassistant.components.deconz.gateway.get_gateway", + side_effect=AuthenticationRequired, + ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: + await setup_deconz_integration(hass) + mock_flow_init.assert_called_once() + + assert hass.data[DECONZ_DOMAIN] == {} + + async def test_reset_after_successful_setup(hass): """Make sure that connection status triggers a dispatcher send.""" config_entry = await setup_deconz_integration(hass) diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index ae7ce5b2a39..d408d764d0e 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -2,6 +2,7 @@ import asyncio from copy import deepcopy +from unittest.mock import patch from homeassistant.components.deconz import ( DeconzGateway, @@ -13,8 +14,6 @@ from homeassistant.components.deconz.gateway import get_gateway_from_config_entr from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration -from tests.async_mock import patch - ENTRY1_HOST = "1.2.3.4" ENTRY1_PORT = 80 ENTRY1_API_KEY = "1234567890ABCDEF" diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 18a135a5e05..20fb50247ee 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -1,6 +1,9 @@ """deCONZ light platform tests.""" from copy import deepcopy +from unittest.mock import patch + +import pytest from homeassistant.components.deconz.const import ( CONF_ALLOW_DECONZ_GROUPS, @@ -33,8 +36,6 @@ from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration -from tests.async_mock import patch - GROUPS = { "1": { "id": "Light group id", @@ -405,3 +406,177 @@ async def test_lidl_christmas_light(hass): ) assert hass.states.get("light.xmas_light") + + +async def test_non_color_light_reports_color(hass): + """Verify hs_color does not crash when a group gets updated with a bad color value. + + After calling a scene color temp light of certain manufacturers + report color temp in color space. + """ + data = deepcopy(DECONZ_WEB_REQUEST) + + data["groups"] = { + "0": { + "action": { + "alert": "none", + "bri": 127, + "colormode": "hs", + "ct": 0, + "effect": "none", + "hue": 0, + "on": True, + "sat": 127, + "scene": None, + "xy": [0, 0], + }, + "devicemembership": [], + "etag": "81e42cf1b47affb72fa72bc2e25ba8bf", + "id": "0", + "lights": ["0", "1"], + "name": "All", + "scenes": [], + "state": {"all_on": False, "any_on": True}, + "type": "LightGroup", + } + } + + data["lights"] = { + "0": { + "ctmax": 500, + "ctmin": 153, + "etag": "026bcfe544ad76c7534e5ca8ed39047c", + "hascolor": True, + "manufacturername": "dresden elektronik", + "modelid": "FLS-PP3", + "name": "Light 1", + "pointsymbol": {}, + "state": { + "alert": None, + "bri": 111, + "colormode": "ct", + "ct": 307, + "effect": None, + "hascolor": True, + "hue": 7998, + "on": False, + "reachable": True, + "sat": 172, + "xy": [0.421253, 0.39921], + }, + "swversion": "020C.201000A0", + "type": "Extended color light", + "uniqueid": "00:21:2E:FF:FF:EE:DD:CC-0A", + }, + "1": { + "colorcapabilities": 0, + "ctmax": 65535, + "ctmin": 0, + "etag": "9dd510cd474791481f189d2a68a3c7f1", + "hascolor": True, + "lastannounced": "2020-12-17T17:44:38Z", + "lastseen": "2021-01-11T18:36Z", + "manufacturername": "IKEA of Sweden", + "modelid": "TRADFRI bulb E27 WS opal 1000lm", + "name": "Küchenlicht", + "state": { + "alert": "none", + "bri": 156, + "colormode": "ct", + "ct": 250, + "on": True, + "reachable": True, + }, + "swversion": "2.0.022", + "type": "Color temperature light", + "uniqueid": "ec:1b:bd:ff:fe:ee:ed:dd-01", + }, + } + config_entry = await setup_deconz_integration(hass, get_state_response=data) + gateway = get_gateway_from_config_entry(hass, config_entry) + + assert len(hass.states.async_all()) == 3 + assert hass.states.get("light.all").attributes[ATTR_COLOR_TEMP] == 307 + + # Updating a scene will return a faulty color value for a non-color light causing an exception in hs_color + state_changed_event = { + "e": "changed", + "id": "1", + "r": "lights", + "state": { + "alert": None, + "bri": 216, + "colormode": "xy", + "ct": 410, + "on": True, + "reachable": True, + }, + "t": "event", + "uniqueid": "ec:1b:bd:ff:fe:ee:ed:dd-01", + } + gateway.api.event_handler(state_changed_event) + await hass.async_block_till_done() + + # Bug is fixed if we reach this point, but device won't have neither color temp nor color + with pytest.raises(KeyError): + assert hass.states.get("light.all").attributes[ATTR_COLOR_TEMP] + assert hass.states.get("light.all").attributes[ATTR_HS_COLOR] + + +async def test_verify_group_supported_features(hass): + """Test that group supported features reflect what included lights support.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["groups"] = deepcopy( + { + "1": { + "id": "Group1", + "name": "group", + "type": "LightGroup", + "state": {"all_on": False, "any_on": True}, + "action": {}, + "scenes": [], + "lights": ["1", "2", "3"], + }, + } + ) + data["lights"] = deepcopy( + { + "1": { + "id": "light1", + "name": "Dimmable light", + "state": {"on": True, "bri": 255, "reachable": True}, + "type": "Light", + "uniqueid": "00:00:00:00:00:00:00:01-00", + }, + "2": { + "id": "light2", + "name": "Color light", + "state": { + "on": True, + "bri": 100, + "colormode": "xy", + "effect": "colorloop", + "xy": (500, 500), + "reachable": True, + }, + "type": "Extended color light", + "uniqueid": "00:00:00:00:00:00:00:02-00", + }, + "3": { + "ctmax": 454, + "ctmin": 155, + "id": "light3", + "name": "Tunable light", + "state": {"on": True, "colormode": "ct", "ct": 2500, "reachable": True}, + "type": "Tunable white light", + "uniqueid": "00:00:00:00:00:00:00:03-00", + }, + } + ) + await setup_deconz_integration(hass, get_state_response=data) + + assert len(hass.states.async_all()) == 4 + + group = hass.states.get("light.group") + assert group.state == STATE_ON + assert group.attributes[ATTR_SUPPORTED_FEATURES] == 63 diff --git a/tests/components/deconz/test_lock.py b/tests/components/deconz/test_lock.py index 2acfb440b13..7e9b8233778 100644 --- a/tests/components/deconz/test_lock.py +++ b/tests/components/deconz/test_lock.py @@ -1,6 +1,7 @@ """deCONZ lock platform tests.""" from copy import deepcopy +from unittest.mock import patch from homeassistant.components.deconz.const import DOMAIN as DECONZ_DOMAIN from homeassistant.components.deconz.gateway import get_gateway_from_config_entry @@ -14,8 +15,6 @@ from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration -from tests.async_mock import patch - LOCKS = { "1": { "etag": "5c2ec06cde4bd654aef3a555fcd8ad12", diff --git a/tests/components/deconz/test_logbook.py b/tests/components/deconz/test_logbook.py new file mode 100644 index 00000000000..7315a766d5c --- /dev/null +++ b/tests/components/deconz/test_logbook.py @@ -0,0 +1,79 @@ +"""The tests for deCONZ logbook.""" + +from copy import deepcopy + +from homeassistant.components import logbook +from homeassistant.components.deconz.deconz_event import CONF_DECONZ_EVENT +from homeassistant.components.deconz.gateway import get_gateway_from_config_entry +from homeassistant.const import CONF_DEVICE_ID, CONF_EVENT, CONF_ID, CONF_UNIQUE_ID +from homeassistant.setup import async_setup_component + +from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration + +from tests.components.logbook.test_init import MockLazyEventPartialState + + +async def test_humanifying_deconz_event(hass): + """Test humanifying deCONZ event.""" + data = deepcopy(DECONZ_WEB_REQUEST) + data["sensors"] = { + "0": { + "id": "Switch 1 id", + "name": "Switch 1", + "type": "ZHASwitch", + "state": {"buttonevent": 1000}, + "config": {}, + "uniqueid": "00:00:00:00:00:00:00:01-00", + }, + "1": { + "id": "Hue remote id", + "name": "Hue remote", + "type": "ZHASwitch", + "modelid": "RWL021", + "state": {"buttonevent": 1000}, + "config": {}, + "uniqueid": "00:00:00:00:00:00:00:02-00", + }, + } + config_entry = await setup_deconz_integration(hass, get_state_response=data) + gateway = get_gateway_from_config_entry(hass, config_entry) + + hass.config.components.add("recorder") + assert await async_setup_component(hass, "logbook", {}) + entity_attr_cache = logbook.EntityAttributeCache(hass) + + events = list( + logbook.humanify( + hass, + [ + MockLazyEventPartialState( + CONF_DECONZ_EVENT, + { + CONF_DEVICE_ID: gateway.events[0].device_id, + CONF_EVENT: 2000, + CONF_ID: gateway.events[0].event_id, + CONF_UNIQUE_ID: gateway.events[0].serial, + }, + ), + MockLazyEventPartialState( + CONF_DECONZ_EVENT, + { + CONF_DEVICE_ID: gateway.events[1].device_id, + CONF_EVENT: 2001, + CONF_ID: gateway.events[1].event_id, + CONF_UNIQUE_ID: gateway.events[1].serial, + }, + ), + ], + entity_attr_cache, + {}, + ) + ) + + assert events[0]["name"] == "Switch 1" + assert events[0]["domain"] == "deconz" + assert events[0]["message"] == "fired event '2000'." + + assert events[1]["name"] == "Hue remote" + assert events[1]["domain"] == "deconz" + assert events[1]["message"] == "'Long press' event for 'Dim up' was fired." diff --git a/tests/components/deconz/test_scene.py b/tests/components/deconz/test_scene.py index c4a92538815..ca8df2c0425 100644 --- a/tests/components/deconz/test_scene.py +++ b/tests/components/deconz/test_scene.py @@ -1,6 +1,7 @@ """deCONZ scene platform tests.""" from copy import deepcopy +from unittest.mock import patch from homeassistant.components.deconz import DOMAIN as DECONZ_DOMAIN from homeassistant.components.deconz.gateway import get_gateway_from_config_entry @@ -10,8 +11,6 @@ from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration -from tests.async_mock import patch - GROUPS = { "1": { "id": "Light group id", diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py index 26524553dbe..faa1d3485bb 100644 --- a/tests/components/deconz/test_services.py +++ b/tests/components/deconz/test_services.py @@ -1,6 +1,7 @@ """deCONZ service tests.""" from copy import deepcopy +from unittest.mock import Mock, patch import pytest import voluptuous as vol @@ -26,8 +27,6 @@ from homeassistant.helpers.entity_registry import async_entries_for_config_entry from .test_gateway import BRIDGEID, DECONZ_WEB_REQUEST, setup_deconz_integration -from tests.async_mock import Mock, patch - GROUP = { "1": { "id": "Group 1 id", diff --git a/tests/components/deconz/test_switch.py b/tests/components/deconz/test_switch.py index 4f23be3d1e3..e42e89d903e 100644 --- a/tests/components/deconz/test_switch.py +++ b/tests/components/deconz/test_switch.py @@ -1,6 +1,7 @@ """deCONZ switch platform tests.""" from copy import deepcopy +from unittest.mock import patch from homeassistant.components.deconz import DOMAIN as DECONZ_DOMAIN from homeassistant.components.deconz.gateway import get_gateway_from_config_entry @@ -14,8 +15,6 @@ from homeassistant.setup import async_setup_component from .test_gateway import DECONZ_WEB_REQUEST, setup_deconz_integration -from tests.async_mock import patch - POWER_PLUGS = { "1": { "id": "On off switch id", diff --git a/tests/components/default_config/test_init.py b/tests/components/default_config/test_init.py index a4a5898982b..9830d944471 100644 --- a/tests/components/default_config/test_init.py +++ b/tests/components/default_config/test_init.py @@ -1,9 +1,10 @@ """Test the default_config init.""" +from unittest.mock import patch + import pytest from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.components.blueprint.conftest import stub_blueprint_populate # noqa diff --git a/tests/components/demo/test_camera.py b/tests/components/demo/test_camera.py index e62d6db0464..8e73a0beff6 100644 --- a/tests/components/demo/test_camera.py +++ b/tests/components/demo/test_camera.py @@ -1,4 +1,6 @@ """The tests for local file camera component.""" +from unittest.mock import patch + import pytest from homeassistant.components.camera import ( @@ -16,8 +18,6 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component -from tests.async_mock import patch - ENTITY_CAMERA = "camera.demo_camera" diff --git a/tests/components/demo/test_geo_location.py b/tests/components/demo/test_geo_location.py index 564d27e7131..c3c233c39be 100644 --- a/tests/components/demo/test_geo_location.py +++ b/tests/components/demo/test_geo_location.py @@ -1,5 +1,7 @@ """The tests for the demo platform.""" +from unittest.mock import patch + import pytest from homeassistant.components import geo_location @@ -16,7 +18,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import assert_setup_component, async_fire_time_changed CONFIG = {geo_location.DOMAIN: [{"platform": "demo"}]} diff --git a/tests/components/demo/test_media_player.py b/tests/components/demo/test_media_player.py index 340416f4586..a32a99bbc63 100644 --- a/tests/components/demo/test_media_player.py +++ b/tests/components/demo/test_media_player.py @@ -1,4 +1,6 @@ """The tests for the Demo Media player platform.""" +from unittest.mock import patch + import pytest import voluptuous as vol @@ -14,8 +16,6 @@ from homeassistant.const import ( from homeassistant.helpers.aiohttp_client import DATA_CLIENTSESSION from homeassistant.setup import async_setup_component -from tests.async_mock import patch - TEST_ENTITY_ID = "media_player.walkman" diff --git a/tests/components/demo/test_notify.py b/tests/components/demo/test_notify.py index 893b9d57e65..7c7f83312dd 100644 --- a/tests/components/demo/test_notify.py +++ b/tests/components/demo/test_notify.py @@ -1,6 +1,7 @@ """The tests for the notify demo platform.""" import logging +from unittest.mock import patch import pytest import voluptuous as vol @@ -11,7 +12,6 @@ from homeassistant.core import callback from homeassistant.helpers import discovery from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import assert_setup_component CONFIG = {notify.DOMAIN: {"platform": "demo"}} diff --git a/tests/components/denonavr/test_config_flow.py b/tests/components/denonavr/test_config_flow.py index b8ea6016131..67d1a4e10db 100644 --- a/tests/components/denonavr/test_config_flow.py +++ b/tests/components/denonavr/test_config_flow.py @@ -1,4 +1,6 @@ """Test the DenonAVR config flow.""" +from unittest.mock import patch + import pytest from homeassistant import config_entries, data_entry_flow @@ -15,7 +17,6 @@ from homeassistant.components.denonavr.config_flow import ( ) from homeassistant.const import CONF_HOST, CONF_MAC -from tests.async_mock import patch from tests.common import MockConfigEntry TEST_HOST = "1.2.3.4" diff --git a/tests/components/derivative/test_sensor.py b/tests/components/derivative/test_sensor.py index 58090f1587d..03861a30c47 100644 --- a/tests/components/derivative/test_sensor.py +++ b/tests/components/derivative/test_sensor.py @@ -1,12 +1,11 @@ """The tests for the derivative sensor platform.""" from datetime import timedelta +from unittest.mock import patch from homeassistant.const import POWER_WATT, TIME_HOURS, TIME_MINUTES, TIME_SECONDS from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch - async def test_state(hass): """Test derivative sensor state.""" diff --git a/tests/components/device_sun_light_trigger/test_init.py b/tests/components/device_sun_light_trigger/test_init.py index 4879be9d18c..7d478e7d8d1 100644 --- a/tests/components/device_sun_light_trigger/test_init.py +++ b/tests/components/device_sun_light_trigger/test_init.py @@ -1,6 +1,7 @@ """The tests device sun light trigger component.""" # pylint: disable=protected-access from datetime import datetime +from unittest.mock import patch import pytest @@ -24,7 +25,6 @@ from homeassistant.core import CoreState from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from tests.async_mock import patch from tests.common import async_fire_time_changed diff --git a/tests/components/device_tracker/test_entities.py b/tests/components/device_tracker/test_entities.py index a0b2553543d..1bc058a1449 100644 --- a/tests/components/device_tracker/test_entities.py +++ b/tests/components/device_tracker/test_entities.py @@ -6,6 +6,9 @@ from homeassistant.components.device_tracker.config_entry import ( ScannerEntity, ) from homeassistant.components.device_tracker.const import ( + ATTR_HOST_NAME, + ATTR_IP, + ATTR_MAC, ATTR_SOURCE_TYPE, DOMAIN, SOURCE_TYPE_ROUTER, @@ -28,6 +31,9 @@ async def test_scanner_entity_device_tracker(hass): assert entity_state.attributes == { ATTR_SOURCE_TYPE: SOURCE_TYPE_ROUTER, ATTR_BATTERY_LEVEL: 100, + ATTR_IP: "0.0.0.0", + ATTR_MAC: "ad:de:ef:be:ed:fe:", + ATTR_HOST_NAME: "test.hostname.org", } assert entity_state.state == STATE_NOT_HOME @@ -49,6 +55,9 @@ def test_scanner_entity(): with pytest.raises(NotImplementedError): assert entity.state == STATE_NOT_HOME assert entity.battery_level is None + assert entity.ip_address is None + assert entity.mac_address is None + assert entity.hostname is None def test_base_tracker_entity(): diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 9424ed229b5..c7aba405ccd 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -3,6 +3,7 @@ from datetime import datetime, timedelta import json import logging import os +from unittest.mock import Mock, call, patch import pytest @@ -27,7 +28,6 @@ from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import Mock, call, patch from tests.common import ( assert_setup_component, async_fire_time_changed, diff --git a/tests/components/devolo_home_control/test_config_flow.py b/tests/components/devolo_home_control/test_config_flow.py index 060188d65aa..dd856d2e6b5 100644 --- a/tests/components/devolo_home_control/test_config_flow.py +++ b/tests/components/devolo_home_control/test_config_flow.py @@ -1,9 +1,10 @@ """Test the devolo_home_control config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.devolo_home_control.const import DOMAIN from homeassistant.config_entries import SOURCE_USER -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/dexcom/__init__.py b/tests/components/dexcom/__init__.py index 47f6238c7c1..16c6f4b4d45 100644 --- a/tests/components/dexcom/__init__.py +++ b/tests/components/dexcom/__init__.py @@ -1,13 +1,13 @@ """Tests for the Dexcom integration.""" import json +from unittest.mock import patch from pydexcom import GlucoseReading from homeassistant.components.dexcom.const import CONF_SERVER, DOMAIN, SERVER_US from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture CONFIG = { diff --git a/tests/components/dexcom/test_config_flow.py b/tests/components/dexcom/test_config_flow.py index c79e7ca0075..2e1dfbcdee5 100644 --- a/tests/components/dexcom/test_config_flow.py +++ b/tests/components/dexcom/test_config_flow.py @@ -1,11 +1,12 @@ """Test the Dexcom config flow.""" +from unittest.mock import patch + from pydexcom import AccountError, SessionError from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.dexcom.const import DOMAIN, MG_DL, MMOL_L from homeassistant.const import CONF_UNIT_OF_MEASUREMENT, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry from tests.components.dexcom import CONFIG diff --git a/tests/components/dexcom/test_init.py b/tests/components/dexcom/test_init.py index 2cb3ad3bf79..a155450bf26 100644 --- a/tests/components/dexcom/test_init.py +++ b/tests/components/dexcom/test_init.py @@ -1,10 +1,11 @@ """Test the Dexcom config flow.""" +from unittest.mock import patch + from pydexcom import AccountError, SessionError from homeassistant.components.dexcom.const import DOMAIN from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED -from tests.async_mock import patch from tests.common import MockConfigEntry from tests.components.dexcom import CONFIG, init_integration diff --git a/tests/components/dexcom/test_sensor.py b/tests/components/dexcom/test_sensor.py index c9e00398140..15de72e9c95 100644 --- a/tests/components/dexcom/test_sensor.py +++ b/tests/components/dexcom/test_sensor.py @@ -1,5 +1,7 @@ """The sensor tests for the griddy platform.""" +from unittest.mock import patch + from pydexcom import SessionError from homeassistant.components.dexcom.const import MMOL_L @@ -9,7 +11,6 @@ from homeassistant.const import ( STATE_UNKNOWN, ) -from tests.async_mock import patch from tests.components.dexcom import GLUCOSE_READING, init_integration diff --git a/tests/components/dhcp/__init__.py b/tests/components/dhcp/__init__.py new file mode 100644 index 00000000000..fc58a7de903 --- /dev/null +++ b/tests/components/dhcp/__init__.py @@ -0,0 +1 @@ +"""Tests for the dhcp integration.""" diff --git a/tests/components/dhcp/test_init.py b/tests/components/dhcp/test_init.py new file mode 100644 index 00000000000..049128248a7 --- /dev/null +++ b/tests/components/dhcp/test_init.py @@ -0,0 +1,533 @@ +"""Test the DHCP discovery integration.""" +import threading +from unittest.mock import patch + +from scapy.error import Scapy_Exception +from scapy.layers.dhcp import DHCP +from scapy.layers.l2 import Ether + +from homeassistant.components import dhcp +from homeassistant.components.device_tracker.const import ( + ATTR_HOST_NAME, + ATTR_IP, + ATTR_MAC, + ATTR_SOURCE_TYPE, + SOURCE_TYPE_ROUTER, +) +from homeassistant.const import ( + EVENT_HOMEASSISTANT_STARTED, + EVENT_HOMEASSISTANT_STOP, + STATE_HOME, + STATE_NOT_HOME, +) +from homeassistant.setup import async_setup_component + +from tests.common import mock_coro + +# connect b8:b7:f1:6d:b5:33 192.168.210.56 +RAW_DHCP_REQUEST = ( + b"\xff\xff\xff\xff\xff\xff\xb8\xb7\xf1m\xb53\x08\x00E\x00\x01P\x06E" + b"\x00\x00\xff\x11\xb4X\x00\x00\x00\x00\xff\xff\xff\xff\x00D\x00C\x01<" + b"\x0b\x14\x01\x01\x06\x00jmjV\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xb8\xb7\xf1m\xb53\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00c\x82Sc5\x01\x039\x02\x05\xdc2\x04\xc0\xa8\xd286" + b"\x04\xc0\xa8\xd0\x017\x04\x01\x03\x1c\x06\x0c\x07connect\xff\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + b"\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" +) + + +async def test_dhcp_match_hostname_and_macaddress(hass): + """Test matching based on hostname and macaddress.""" + dhcp_watcher = dhcp.DHCPWatcher( + hass, + {}, + [{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}], + ) + + packet = Ether(RAW_DHCP_REQUEST) + + with patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + dhcp_watcher.handle_dhcp_packet(packet) + # Ensure no change is ignored + dhcp_watcher.handle_dhcp_packet(packet) + + assert len(mock_init.mock_calls) == 1 + assert mock_init.mock_calls[0][1][0] == "mock-domain" + assert mock_init.mock_calls[0][2]["context"] == {"source": "dhcp"} + assert mock_init.mock_calls[0][2]["data"] == { + dhcp.IP_ADDRESS: "192.168.210.56", + dhcp.HOSTNAME: "connect", + dhcp.MAC_ADDRESS: "b8b7f16db533", + } + + +async def test_dhcp_match_hostname(hass): + """Test matching based on hostname only.""" + dhcp_watcher = dhcp.DHCPWatcher( + hass, {}, [{"domain": "mock-domain", "hostname": "connect"}] + ) + + packet = Ether(RAW_DHCP_REQUEST) + + with patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + dhcp_watcher.handle_dhcp_packet(packet) + + assert len(mock_init.mock_calls) == 1 + assert mock_init.mock_calls[0][1][0] == "mock-domain" + assert mock_init.mock_calls[0][2]["context"] == {"source": "dhcp"} + assert mock_init.mock_calls[0][2]["data"] == { + dhcp.IP_ADDRESS: "192.168.210.56", + dhcp.HOSTNAME: "connect", + dhcp.MAC_ADDRESS: "b8b7f16db533", + } + + +async def test_dhcp_match_macaddress(hass): + """Test matching based on macaddress only.""" + dhcp_watcher = dhcp.DHCPWatcher( + hass, {}, [{"domain": "mock-domain", "macaddress": "B8B7F1*"}] + ) + + packet = Ether(RAW_DHCP_REQUEST) + + with patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + dhcp_watcher.handle_dhcp_packet(packet) + + assert len(mock_init.mock_calls) == 1 + assert mock_init.mock_calls[0][1][0] == "mock-domain" + assert mock_init.mock_calls[0][2]["context"] == {"source": "dhcp"} + assert mock_init.mock_calls[0][2]["data"] == { + dhcp.IP_ADDRESS: "192.168.210.56", + dhcp.HOSTNAME: "connect", + dhcp.MAC_ADDRESS: "b8b7f16db533", + } + + +async def test_dhcp_nomatch(hass): + """Test not matching based on macaddress only.""" + dhcp_watcher = dhcp.DHCPWatcher( + hass, {}, [{"domain": "mock-domain", "macaddress": "ABC123*"}] + ) + + packet = Ether(RAW_DHCP_REQUEST) + + with patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + dhcp_watcher.handle_dhcp_packet(packet) + + assert len(mock_init.mock_calls) == 0 + + +async def test_dhcp_nomatch_hostname(hass): + """Test not matching based on hostname only.""" + dhcp_watcher = dhcp.DHCPWatcher( + hass, {}, [{"domain": "mock-domain", "hostname": "nomatch*"}] + ) + + packet = Ether(RAW_DHCP_REQUEST) + + with patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + dhcp_watcher.handle_dhcp_packet(packet) + + assert len(mock_init.mock_calls) == 0 + + +async def test_dhcp_nomatch_non_dhcp_packet(hass): + """Test matching does not throw on a non-dhcp packet.""" + dhcp_watcher = dhcp.DHCPWatcher( + hass, {}, [{"domain": "mock-domain", "hostname": "nomatch*"}] + ) + + packet = Ether(b"") + + with patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + dhcp_watcher.handle_dhcp_packet(packet) + + assert len(mock_init.mock_calls) == 0 + + +async def test_dhcp_nomatch_non_dhcp_request_packet(hass): + """Test nothing happens with the wrong message-type.""" + dhcp_watcher = dhcp.DHCPWatcher( + hass, {}, [{"domain": "mock-domain", "hostname": "nomatch*"}] + ) + + packet = Ether(RAW_DHCP_REQUEST) + + packet[DHCP].options = [ + ("message-type", 4), + ("max_dhcp_size", 1500), + ("requested_addr", "192.168.210.56"), + ("server_id", "192.168.208.1"), + ("param_req_list", [1, 3, 28, 6]), + ("hostname", b"connect"), + ] + + with patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + dhcp_watcher.handle_dhcp_packet(packet) + + assert len(mock_init.mock_calls) == 0 + + +async def test_dhcp_invalid_hostname(hass): + """Test we ignore invalid hostnames.""" + dhcp_watcher = dhcp.DHCPWatcher( + hass, {}, [{"domain": "mock-domain", "hostname": "nomatch*"}] + ) + + packet = Ether(RAW_DHCP_REQUEST) + + packet[DHCP].options = [ + ("message-type", 3), + ("max_dhcp_size", 1500), + ("requested_addr", "192.168.210.56"), + ("server_id", "192.168.208.1"), + ("param_req_list", [1, 3, 28, 6]), + ("hostname", "connect"), + ] + + with patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + dhcp_watcher.handle_dhcp_packet(packet) + + assert len(mock_init.mock_calls) == 0 + + +async def test_dhcp_missing_hostname(hass): + """Test we ignore missing hostnames.""" + dhcp_watcher = dhcp.DHCPWatcher( + hass, {}, [{"domain": "mock-domain", "hostname": "nomatch*"}] + ) + + packet = Ether(RAW_DHCP_REQUEST) + + packet[DHCP].options = [ + ("message-type", 3), + ("max_dhcp_size", 1500), + ("requested_addr", "192.168.210.56"), + ("server_id", "192.168.208.1"), + ("param_req_list", [1, 3, 28, 6]), + ("hostname", None), + ] + + with patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + dhcp_watcher.handle_dhcp_packet(packet) + + assert len(mock_init.mock_calls) == 0 + + +async def test_dhcp_invalid_option(hass): + """Test we ignore invalid hostname option.""" + dhcp_watcher = dhcp.DHCPWatcher( + hass, {}, [{"domain": "mock-domain", "hostname": "nomatch*"}] + ) + + packet = Ether(RAW_DHCP_REQUEST) + + packet[DHCP].options = [ + ("message-type", 3), + ("max_dhcp_size", 1500), + ("requested_addr", "192.168.208.55"), + ("server_id", "192.168.208.1"), + ("param_req_list", [1, 3, 28, 6]), + ("hostname"), + ] + + with patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + dhcp_watcher.handle_dhcp_packet(packet) + + assert len(mock_init.mock_calls) == 0 + + +async def test_setup_and_stop(hass): + """Test we can setup and stop.""" + + assert await async_setup_component( + hass, + dhcp.DOMAIN, + {}, + ) + await hass.async_block_till_done() + + with patch("homeassistant.components.dhcp.AsyncSniffer.start") as start_call: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + + start_call.assert_called_once() + + +async def test_setup_fails_as_root(hass, caplog): + """Test we handle sniff setup failing as root.""" + + assert await async_setup_component( + hass, + dhcp.DOMAIN, + {}, + ) + await hass.async_block_till_done() + + wait_event = threading.Event() + + with patch("os.geteuid", return_value=0), patch( + "homeassistant.components.dhcp._verify_l2socket_creation_permission", + side_effect=Scapy_Exception, + ): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + wait_event.set() + assert "Cannot watch for dhcp packets" in caplog.text + + +async def test_setup_fails_non_root(hass, caplog): + """Test we handle sniff setup failing as non-root.""" + + assert await async_setup_component( + hass, + dhcp.DOMAIN, + {}, + ) + await hass.async_block_till_done() + + wait_event = threading.Event() + + with patch("os.geteuid", return_value=10), patch( + "homeassistant.components.dhcp._verify_l2socket_creation_permission", + side_effect=Scapy_Exception, + ): + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + await hass.async_block_till_done() + wait_event.set() + assert "Cannot watch for dhcp packets without root or CAP_NET_RAW" in caplog.text + + +async def test_device_tracker_hostname_and_macaddress_exists_before_start(hass): + """Test matching based on hostname and macaddress before start.""" + hass.states.async_set( + "device_tracker.august_connect", + STATE_HOME, + { + ATTR_HOST_NAME: "connect", + ATTR_IP: "192.168.210.56", + ATTR_SOURCE_TYPE: SOURCE_TYPE_ROUTER, + ATTR_MAC: "B8:B7:F1:6D:B5:33", + }, + ) + + with patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + device_tracker_watcher = dhcp.DeviceTrackerWatcher( + hass, + {}, + [{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}], + ) + await device_tracker_watcher.async_start() + await hass.async_block_till_done() + await device_tracker_watcher.async_stop() + await hass.async_block_till_done() + + assert len(mock_init.mock_calls) == 1 + assert mock_init.mock_calls[0][1][0] == "mock-domain" + assert mock_init.mock_calls[0][2]["context"] == {"source": "dhcp"} + assert mock_init.mock_calls[0][2]["data"] == { + dhcp.IP_ADDRESS: "192.168.210.56", + dhcp.HOSTNAME: "connect", + dhcp.MAC_ADDRESS: "b8b7f16db533", + } + + +async def test_device_tracker_hostname_and_macaddress_after_start(hass): + """Test matching based on hostname and macaddress after start.""" + + with patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + device_tracker_watcher = dhcp.DeviceTrackerWatcher( + hass, + {}, + [{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}], + ) + await device_tracker_watcher.async_start() + await hass.async_block_till_done() + hass.states.async_set( + "device_tracker.august_connect", + STATE_HOME, + { + ATTR_HOST_NAME: "connect", + ATTR_IP: "192.168.210.56", + ATTR_SOURCE_TYPE: SOURCE_TYPE_ROUTER, + ATTR_MAC: "B8:B7:F1:6D:B5:33", + }, + ) + await hass.async_block_till_done() + await device_tracker_watcher.async_stop() + await hass.async_block_till_done() + + assert len(mock_init.mock_calls) == 1 + assert mock_init.mock_calls[0][1][0] == "mock-domain" + assert mock_init.mock_calls[0][2]["context"] == {"source": "dhcp"} + assert mock_init.mock_calls[0][2]["data"] == { + dhcp.IP_ADDRESS: "192.168.210.56", + dhcp.HOSTNAME: "connect", + dhcp.MAC_ADDRESS: "b8b7f16db533", + } + + +async def test_device_tracker_hostname_and_macaddress_after_start_not_home(hass): + """Test matching based on hostname and macaddress after start but not home.""" + + with patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + device_tracker_watcher = dhcp.DeviceTrackerWatcher( + hass, + {}, + [{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}], + ) + await device_tracker_watcher.async_start() + await hass.async_block_till_done() + hass.states.async_set( + "device_tracker.august_connect", + STATE_NOT_HOME, + { + ATTR_HOST_NAME: "connect", + ATTR_IP: "192.168.210.56", + ATTR_SOURCE_TYPE: SOURCE_TYPE_ROUTER, + ATTR_MAC: "B8:B7:F1:6D:B5:33", + }, + ) + await hass.async_block_till_done() + await device_tracker_watcher.async_stop() + await hass.async_block_till_done() + + assert len(mock_init.mock_calls) == 0 + + +async def test_device_tracker_hostname_and_macaddress_after_start_not_router(hass): + """Test matching based on hostname and macaddress after start but not router.""" + + with patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + device_tracker_watcher = dhcp.DeviceTrackerWatcher( + hass, + {}, + [{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}], + ) + await device_tracker_watcher.async_start() + await hass.async_block_till_done() + hass.states.async_set( + "device_tracker.august_connect", + STATE_HOME, + { + ATTR_HOST_NAME: "connect", + ATTR_IP: "192.168.210.56", + ATTR_SOURCE_TYPE: "something_else", + ATTR_MAC: "B8:B7:F1:6D:B5:33", + }, + ) + await hass.async_block_till_done() + await device_tracker_watcher.async_stop() + await hass.async_block_till_done() + + assert len(mock_init.mock_calls) == 0 + + +async def test_device_tracker_hostname_and_macaddress_after_start_hostname_missing( + hass, +): + """Test matching based on hostname and macaddress after start but missing hostname.""" + + with patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + device_tracker_watcher = dhcp.DeviceTrackerWatcher( + hass, + {}, + [{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}], + ) + await device_tracker_watcher.async_start() + await hass.async_block_till_done() + hass.states.async_set( + "device_tracker.august_connect", + STATE_HOME, + { + ATTR_IP: "192.168.210.56", + ATTR_SOURCE_TYPE: SOURCE_TYPE_ROUTER, + ATTR_MAC: "B8:B7:F1:6D:B5:33", + }, + ) + await hass.async_block_till_done() + await device_tracker_watcher.async_stop() + await hass.async_block_till_done() + + assert len(mock_init.mock_calls) == 0 + + +async def test_device_tracker_ignore_self_assigned_ips_before_start(hass): + """Test matching ignores self assigned ip address.""" + hass.states.async_set( + "device_tracker.august_connect", + STATE_HOME, + { + ATTR_HOST_NAME: "connect", + ATTR_IP: "169.254.210.56", + ATTR_SOURCE_TYPE: SOURCE_TYPE_ROUTER, + ATTR_MAC: "B8:B7:F1:6D:B5:33", + }, + ) + + with patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + device_tracker_watcher = dhcp.DeviceTrackerWatcher( + hass, + {}, + [{"domain": "mock-domain", "hostname": "connect", "macaddress": "B8B7F1*"}], + ) + await device_tracker_watcher.async_start() + await hass.async_block_till_done() + await device_tracker_watcher.async_stop() + await hass.async_block_till_done() + + assert len(mock_init.mock_calls) == 0 diff --git a/tests/components/directv/test_config_flow.py b/tests/components/directv/test_config_flow.py index 09b76cfb550..3372d48aadb 100644 --- a/tests/components/directv/test_config_flow.py +++ b/tests/components/directv/test_config_flow.py @@ -1,4 +1,6 @@ """Test the DirecTV config flow.""" +from unittest.mock import patch + from aiohttp import ClientError as HTTPClientError from homeassistant.components.directv.const import CONF_RECEIVER_ID, DOMAIN @@ -12,7 +14,6 @@ from homeassistant.data_entry_flow import ( ) from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import patch from tests.components.directv import ( HOST, MOCK_SSDP_DISCOVERY_INFO, diff --git a/tests/components/directv/test_media_player.py b/tests/components/directv/test_media_player.py index 11aaad707b7..506cf62a44d 100644 --- a/tests/components/directv/test_media_player.py +++ b/tests/components/directv/test_media_player.py @@ -1,6 +1,7 @@ """The tests for the DirecTV Media player platform.""" from datetime import datetime, timedelta from typing import Optional +from unittest.mock import patch from pytest import fixture @@ -55,7 +56,6 @@ from homeassistant.const import ( from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import dt as dt_util -from tests.async_mock import patch from tests.components.directv import setup_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/directv/test_remote.py b/tests/components/directv/test_remote.py index b00f62c0e0c..33521958747 100644 --- a/tests/components/directv/test_remote.py +++ b/tests/components/directv/test_remote.py @@ -1,4 +1,6 @@ """The tests for the DirecTV remote platform.""" +from unittest.mock import patch + from homeassistant.components.remote import ( ATTR_COMMAND, DOMAIN as REMOTE_DOMAIN, @@ -7,7 +9,6 @@ from homeassistant.components.remote import ( from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import patch from tests.components.directv import setup_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/discovery/test_init.py b/tests/components/discovery/test_init.py index 8003f83d996..fd66e59ef21 100644 --- a/tests/components/discovery/test_init.py +++ b/tests/components/discovery/test_init.py @@ -1,5 +1,5 @@ """The tests for the discovery component.""" -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch import pytest @@ -9,7 +9,6 @@ from homeassistant.components import discovery from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.util.dt import utcnow -from tests.async_mock import patch from tests.common import async_fire_time_changed, mock_coro # One might consider to "mock" services, but it's easy enough to just use diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py index c52388b886c..e94f73239f1 100644 --- a/tests/components/doorbird/test_config_flow.py +++ b/tests/components/doorbird/test_config_flow.py @@ -1,4 +1,5 @@ """Test the DoorBird config flow.""" +from unittest.mock import MagicMock, patch import urllib from homeassistant import config_entries, data_entry_flow, setup @@ -6,7 +7,6 @@ from homeassistant.components.doorbird import CONF_CUSTOM_URL, CONF_TOKEN from homeassistant.components.doorbird.const import CONF_EVENTS, DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry, init_recorder_component VALID_CONFIG = { diff --git a/tests/components/dsmr/conftest.py b/tests/components/dsmr/conftest.py index d57828fdfa4..ab7b3a4d479 100644 --- a/tests/components/dsmr/conftest.py +++ b/tests/components/dsmr/conftest.py @@ -1,5 +1,6 @@ """Common test tools.""" import asyncio +from unittest.mock import MagicMock, patch from dsmr_parser.clients.protocol import DSMRProtocol from dsmr_parser.obis_references import ( @@ -10,8 +11,6 @@ from dsmr_parser.obis_references import ( from dsmr_parser.objects import CosemObject import pytest -from tests.async_mock import MagicMock, patch - @pytest.fixture async def dsmr_connection_fixture(hass): diff --git a/tests/components/dsmr/test_config_flow.py b/tests/components/dsmr/test_config_flow.py index 9ae49419bf4..edb3810e24f 100644 --- a/tests/components/dsmr/test_config_flow.py +++ b/tests/components/dsmr/test_config_flow.py @@ -1,13 +1,13 @@ """Test the DSMR config flow.""" import asyncio from itertools import chain, repeat +from unittest.mock import DEFAULT, AsyncMock, patch import serial from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.dsmr import DOMAIN -from tests.async_mock import DEFAULT, AsyncMock, patch from tests.common import MockConfigEntry SERIAL_DATA = {"serial_id": "12345678", "serial_id_gas": "123456789"} diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 76a9a5bb070..dde66c6bfb7 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -9,6 +9,7 @@ import asyncio import datetime from decimal import Decimal from itertools import chain, repeat +from unittest.mock import DEFAULT, MagicMock from homeassistant.components.dsmr.const import DOMAIN from homeassistant.components.dsmr.sensor import DerivativeDSMREntity @@ -20,7 +21,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import DEFAULT, MagicMock from tests.common import MockConfigEntry, patch diff --git a/tests/components/dunehd/test_config_flow.py b/tests/components/dunehd/test_config_flow.py index f2d30b2a8dd..b6e57a71668 100644 --- a/tests/components/dunehd/test_config_flow.py +++ b/tests/components/dunehd/test_config_flow.py @@ -1,10 +1,11 @@ """Define tests for the Dune HD config flow.""" +from unittest.mock import patch + from homeassistant import data_entry_flow from homeassistant.components.dunehd.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_HOST -from tests.async_mock import patch from tests.common import MockConfigEntry CONFIG_HOSTNAME = {CONF_HOST: "dunehd-host"} diff --git a/tests/components/dynalite/common.py b/tests/components/dynalite/common.py index f72e3f481b6..48ec378689e 100644 --- a/tests/components/dynalite/common.py +++ b/tests/components/dynalite/common.py @@ -1,8 +1,9 @@ """Common functions for tests.""" +from unittest.mock import AsyncMock, Mock, call, patch + from homeassistant.components import dynalite from homeassistant.helpers import entity_registry -from tests.async_mock import AsyncMock, Mock, call, patch from tests.common import MockConfigEntry ATTR_SERVICE = "service" diff --git a/tests/components/dynalite/test_bridge.py b/tests/components/dynalite/test_bridge.py index 8f3210bbcf8..363a9671f59 100644 --- a/tests/components/dynalite/test_bridge.py +++ b/tests/components/dynalite/test_bridge.py @@ -1,6 +1,8 @@ """Test Dynalite bridge.""" +from unittest.mock import AsyncMock, Mock, patch + from dynalite_devices_lib.dynalite_devices import ( CONF_AREA as dyn_CONF_AREA, CONF_PRESET as dyn_CONF_PRESET, @@ -18,7 +20,6 @@ from homeassistant.components.dynalite.const import ( ) from homeassistant.helpers.dispatcher import async_dispatcher_connect -from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/dynalite/test_config_flow.py b/tests/components/dynalite/test_config_flow.py index 11b4d6b524c..e21c82d7c20 100644 --- a/tests/components/dynalite/test_config_flow.py +++ b/tests/components/dynalite/test_config_flow.py @@ -1,11 +1,12 @@ """Test Dynalite config flow.""" +from unittest.mock import AsyncMock, patch + import pytest from homeassistant import config_entries from homeassistant.components import dynalite -from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry diff --git a/tests/components/dynalite/test_init.py b/tests/components/dynalite/test_init.py index faa75aadef8..d231f82d2f8 100644 --- a/tests/components/dynalite/test_init.py +++ b/tests/components/dynalite/test_init.py @@ -1,6 +1,8 @@ """Test Dynalite __init__.""" +from unittest.mock import call, patch + import pytest from voluptuous import MultipleInvalid @@ -8,7 +10,6 @@ import homeassistant.components.dynalite.const as dynalite from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_ROOM from homeassistant.setup import async_setup_component -from tests.async_mock import call, patch from tests.common import MockConfigEntry diff --git a/tests/components/dyson/common.py b/tests/components/dyson/common.py index f1dabe5203d..b26c48d55f8 100644 --- a/tests/components/dyson/common.py +++ b/tests/components/dyson/common.py @@ -1,26 +1,103 @@ """Common utils for Dyson tests.""" +from typing import Optional, Type from unittest import mock +from unittest.mock import MagicMock -from libpurecool.dyson_pure_cool import FanSpeed +from libpurecool.const import SLEEP_TIMER_OFF, Dyson360EyeMode, FanMode, PowerMode +from libpurecool.dyson_360_eye import Dyson360Eye +from libpurecool.dyson_device import DysonDevice +from libpurecool.dyson_pure_cool import DysonPureCool, FanSpeed +from libpurecool.dyson_pure_cool_link import DysonPureCoolLink + +from homeassistant.components.dyson import CONF_LANGUAGE, DOMAIN +from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant, callback + +SERIAL = "XX-XXXXX-XX" +NAME = "Temp Name" +ENTITY_NAME = "temp_name" +IP_ADDRESS = "0.0.0.0" + +BASE_PATH = "homeassistant.components.dyson" + +CONFIG = { + DOMAIN: { + CONF_USERNAME: "user@example.com", + CONF_PASSWORD: "password", + CONF_LANGUAGE: "US", + CONF_DEVICES: [ + { + "device_id": SERIAL, + "device_ip": IP_ADDRESS, + } + ], + } +} -def load_mock_device(device): - """Load the mock with default values so it doesn't throw errors.""" - device.serial = "XX-XXXXX-XX" - device.name = "Temp Name" +@callback +def async_get_basic_device(spec: Type[DysonDevice]) -> DysonDevice: + """Return a basic device with common fields filled out.""" + device = MagicMock(spec=spec) + device.serial = SERIAL + device.name = NAME device.connect = mock.Mock(return_value=True) device.auto_connect = mock.Mock(return_value=True) - device.environmental_state.particulate_matter_25 = "0000" - device.environmental_state.particulate_matter_10 = "0000" - device.environmental_state.nitrogen_dioxide = "0000" - device.environmental_state.volatil_organic_compounds = "0000" - device.environmental_state.volatile_organic_compounds = "0000" - device.environmental_state.temperature = 250 - device.state.hepa_filter_state = 0 - device.state.carbon_filter_state = 0 + return device + + +@callback +def async_get_360eye_device(state=Dyson360EyeMode.FULL_CLEAN_RUNNING) -> Dyson360Eye: + """Return a Dyson 360 Eye device.""" + device = async_get_basic_device(Dyson360Eye) + device.state.state = state + device.state.battery_level = 85 + device.state.power_mode = PowerMode.QUIET + device.state.position = (0, 0) + return device + + +@callback +def async_get_purecoollink_device() -> DysonPureCoolLink: + """Return a Dyson Pure Cool Link device.""" + device = async_get_basic_device(DysonPureCoolLink) + device.state.fan_mode = FanMode.FAN.value device.state.speed = FanSpeed.FAN_SPEED_1.value - device.state.oscillation_angle_low = "000" - device.state.oscillation_angle_high = "000" - device.state.filter_life = "000" - device.state.heat_target = 200 + device.state.night_mode = "ON" + device.state.oscillation = "ON" + return device + + +@callback +def async_get_purecool_device() -> DysonPureCool: + """Return a Dyson Pure Cool device.""" + device = async_get_basic_device(DysonPureCool) + device.state.fan_power = "ON" + device.state.speed = FanSpeed.FAN_SPEED_1.value + device.state.night_mode = "ON" + device.state.oscillation = "OION" + device.state.oscillation_angle_low = "0024" + device.state.oscillation_angle_high = "0254" + device.state.auto_mode = "OFF" + device.state.front_direction = "ON" + device.state.sleep_timer = SLEEP_TIMER_OFF + device.state.hepa_filter_state = "0100" + device.state.carbon_filter_state = "0100" + return device + + +async def async_update_device( + hass: HomeAssistant, device: DysonDevice, state_type: Optional[Type] = None +) -> None: + """Update the device using callback function.""" + callbacks = [args[0][0] for args in device.add_message_listener.call_args_list] + message = MagicMock(spec=state_type) + + # Combining sync calls to avoid multiple executors + def _run_callbacks(): + for callback_fn in callbacks: + callback_fn(message) + + await hass.async_add_executor_job(_run_callbacks) + await hass.async_block_till_done() diff --git a/tests/components/dyson/conftest.py b/tests/components/dyson/conftest.py new file mode 100644 index 00000000000..747f7a43986 --- /dev/null +++ b/tests/components/dyson/conftest.py @@ -0,0 +1,38 @@ +"""Configure pytest for Dyson tests.""" +from unittest.mock import patch + +from libpurecool.dyson_device import DysonDevice +import pytest + +from homeassistant.components.dyson import DOMAIN +from homeassistant.core import HomeAssistant + +from .common import BASE_PATH, CONFIG + +from tests.common import async_setup_component + + +@pytest.fixture() +async def device(hass: HomeAssistant, request) -> DysonDevice: + """Fixture to provide Dyson 360 Eye device.""" + platform = request.module.PLATFORM_DOMAIN + get_device = request.module.async_get_device + if hasattr(request, "param"): + if isinstance(request.param, list): + device = get_device(*request.param) + else: + device = get_device(request.param) + else: + device = get_device() + with patch(f"{BASE_PATH}.DysonAccount.login", return_value=True), patch( + f"{BASE_PATH}.DysonAccount.devices", return_value=[device] + ), patch(f"{BASE_PATH}.DYSON_PLATFORMS", [platform]): + # DYSON_PLATFORMS is patched so that only the platform being tested is set up + await async_setup_component( + hass, + DOMAIN, + CONFIG, + ) + await hass.async_block_till_done() + + return device diff --git a/tests/components/dyson/test_air_quality.py b/tests/components/dyson/test_air_quality.py index ab11a1ad897..51b38303a58 100644 --- a/tests/components/dyson/test_air_quality.py +++ b/tests/components/dyson/test_air_quality.py @@ -1,160 +1,67 @@ """Test the Dyson air quality component.""" -import json -from unittest import mock from libpurecool.dyson_pure_cool import DysonPureCool from libpurecool.dyson_pure_state_v2 import DysonEnvironmentalSensorV2State -from homeassistant.components import dyson as dyson_parent from homeassistant.components.air_quality import ( + ATTR_AQI, ATTR_NO2, ATTR_PM_2_5, ATTR_PM_10, - DOMAIN as AIQ_DOMAIN, + DOMAIN as PLATFORM_DOMAIN, ) -import homeassistant.components.dyson.air_quality as dyson -from homeassistant.helpers import discovery -from homeassistant.setup import async_setup_component +from homeassistant.components.dyson.air_quality import ATTR_VOC +from homeassistant.core import HomeAssistant, callback -from .common import load_mock_device +from .common import ENTITY_NAME, async_get_purecool_device, async_update_device -from tests.async_mock import patch +ENTITY_ID = f"{PLATFORM_DOMAIN}.{ENTITY_NAME}" + +MOCKED_VALUES = { + ATTR_PM_2_5: 10, + ATTR_PM_10: 20, + ATTR_NO2: 30, + ATTR_VOC: 40, +} + +MOCKED_UPDATED_VALUES = { + ATTR_PM_2_5: 60, + ATTR_PM_10: 50, + ATTR_NO2: 40, + ATTR_VOC: 30, +} -def _get_dyson_purecool_device(): - """Return a valid device as provided by the Dyson web services.""" - device = mock.Mock(spec=DysonPureCool) - load_mock_device(device) - device.name = "Living room" - device.environmental_state.particulate_matter_25 = "0014" - device.environmental_state.particulate_matter_10 = "0025" - device.environmental_state.nitrogen_dioxide = "0042" - device.environmental_state.volatile_organic_compounds = "0035" +def _async_assign_values(device: DysonPureCool, values=MOCKED_VALUES) -> None: + """Assign mocked environmental states to the device.""" + device.environmental_state.particulate_matter_25 = values[ATTR_PM_2_5] + device.environmental_state.particulate_matter_10 = values[ATTR_PM_10] + device.environmental_state.nitrogen_dioxide = values[ATTR_NO2] + device.environmental_state.volatile_organic_compounds = values[ATTR_VOC] + + +@callback +def async_get_device() -> DysonPureCool: + """Return a device of the given type.""" + device = async_get_purecool_device() + _async_assign_values(device) return device -def _get_config(): - """Return a config dictionary.""" - return { - dyson_parent.DOMAIN: { - dyson_parent.CONF_USERNAME: "email", - dyson_parent.CONF_PASSWORD: "password", - dyson_parent.CONF_LANGUAGE: "GB", - dyson_parent.CONF_DEVICES: [ - {"device_id": "XX-XXXXX-XX", "device_ip": "192.168.0.1"} - ], - } - } +async def test_air_quality(hass: HomeAssistant, device: DysonPureCool) -> None: + """Test the state and attributes of the air quality entity.""" + state = hass.states.get(ENTITY_ID) + assert state.state == str(MOCKED_VALUES[ATTR_PM_2_5]) + attributes = state.attributes + for attr, value in MOCKED_VALUES.items(): + assert attributes[attr] == value + assert attributes[ATTR_AQI] == 40 - -@patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_dyson_purecool_device()], -) -async def test_purecool_aiq_attributes(devices, login, hass): - """Test state attributes.""" - await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) - await hass.async_block_till_done() - fan_state = hass.states.get("air_quality.living_room") - attributes = fan_state.attributes - - assert fan_state.state == "14" - assert attributes[ATTR_PM_2_5] == 14 - assert attributes[ATTR_PM_10] == 25 - assert attributes[ATTR_NO2] == 42 - assert attributes[dyson.ATTR_VOC] == 35 - - -@patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_dyson_purecool_device()], -) -async def test_purecool_aiq_update_state(devices, login, hass): - """Test state update.""" - device = devices.return_value[0] - await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) - await hass.async_block_till_done() - event = { - "msg": "ENVIRONMENTAL-CURRENT-SENSOR-DATA", - "time": "2019-03-29T10:00:01.000Z", - "data": { - "pm10": "0080", - "p10r": "0151", - "hact": "0040", - "va10": "0055", - "p25r": "0161", - "noxl": "0069", - "pm25": "0035", - "sltm": "OFF", - "tact": "2960", - }, - } - device.environmental_state = DysonEnvironmentalSensorV2State(json.dumps(event)) - - for call in device.add_message_listener.call_args_list: - callback = call[0][0] - if type(callback.__self__) == dyson.DysonAirSensor: - callback(device.environmental_state) - - await hass.async_block_till_done() - fan_state = hass.states.get("air_quality.living_room") - attributes = fan_state.attributes - - assert fan_state.state == "35" - assert attributes[ATTR_PM_2_5] == 35 - assert attributes[ATTR_PM_10] == 80 - assert attributes[ATTR_NO2] == 69 - assert attributes[dyson.ATTR_VOC] == 55 - - -@patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_dyson_purecool_device()], -) -async def test_purecool_component_setup_only_once(devices, login, hass): - """Test if entities are created only once.""" - config = _get_config() - await async_setup_component(hass, dyson_parent.DOMAIN, config) - await hass.async_block_till_done() - discovery.load_platform(hass, AIQ_DOMAIN, dyson_parent.DOMAIN, {}, config) - await hass.async_block_till_done() - - assert len(hass.data[dyson.DYSON_AIQ_DEVICES]) == 1 - - -@patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_dyson_purecool_device()], -) -async def test_purecool_aiq_without_discovery(devices, login, hass): - """Test if component correctly returns if discovery not set.""" - await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) - await hass.async_block_till_done() - add_entities_mock = mock.MagicMock() - - dyson.setup_platform(hass, None, add_entities_mock, None) - - assert add_entities_mock.call_count == 0 - - -@patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_dyson_purecool_device()], -) -async def test_purecool_aiq_empty_environment_state(devices, login, hass): - """Test device with empty environmental state.""" - await async_setup_component(hass, dyson_parent.DOMAIN, _get_config()) - await hass.async_block_till_done() - device = hass.data[dyson.DYSON_AIQ_DEVICES][0] - device._device.environmental_state = None - - assert device.state is None - assert device.particulate_matter_2_5 is None - assert device.particulate_matter_10 is None - assert device.nitrogen_dioxide is None - assert device.volatile_organic_compounds is None + _async_assign_values(device, MOCKED_UPDATED_VALUES) + await async_update_device(hass, device, DysonEnvironmentalSensorV2State) + state = hass.states.get(ENTITY_ID) + assert state.state == str(MOCKED_UPDATED_VALUES[ATTR_PM_2_5]) + attributes = state.attributes + for attr, value in MOCKED_UPDATED_VALUES.items(): + assert attributes[attr] == value + assert attributes[ATTR_AQI] == 60 diff --git a/tests/components/dyson/test_climate.py b/tests/components/dyson/test_climate.py index c4e4c91087c..0e389000c29 100644 --- a/tests/components/dyson/test_climate.py +++ b/tests/components/dyson/test_climate.py @@ -1,27 +1,24 @@ """Test the Dyson fan component.""" -import json + +from typing import Type from libpurecool.const import ( + AutoMode, FanPower, FanSpeed, FanState, FocusMode, HeatMode, HeatState, - HeatTarget, ) +from libpurecool.dyson_device import DysonDevice from libpurecool.dyson_pure_hotcool import DysonPureHotCool from libpurecool.dyson_pure_hotcool_link import DysonPureHotCoolLink from libpurecool.dyson_pure_state import DysonPureHotCoolState from libpurecool.dyson_pure_state_v2 import DysonPureHotCoolV2State import pytest -from homeassistant.components.climate import ( - DOMAIN, - SERVICE_SET_FAN_MODE, - SERVICE_SET_HVAC_MODE, - SERVICE_SET_TEMPERATURE, -) +from homeassistant.components.climate import DOMAIN as PLATFORM_DOMAIN from homeassistant.components.climate.const import ( ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, @@ -32,12 +29,13 @@ from homeassistant.components.climate.const import ( ATTR_HVAC_MODES, ATTR_MAX_TEMP, ATTR_MIN_TEMP, - ATTR_TARGET_TEMP_HIGH, - ATTR_TARGET_TEMP_LOW, CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, FAN_AUTO, + FAN_DIFFUSE, + FAN_FOCUS, FAN_HIGH, FAN_LOW, FAN_MEDIUM, @@ -45,682 +43,307 @@ from homeassistant.components.climate.const import ( HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF, + SERVICE_SET_FAN_MODE, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_TEMPERATURE, +) +from homeassistant.components.dyson.climate import ( + SUPPORT_FAN, + SUPPORT_FAN_PCOOL, + SUPPORT_FLAGS, + SUPPORT_HVAC, + SUPPORT_HVAC_PCOOL, ) -from homeassistant.components.dyson import CONF_LANGUAGE, DOMAIN as DYSON_DOMAIN -from homeassistant.components.dyson.climate import FAN_DIFFUSE, FAN_FOCUS, SUPPORT_FLAGS from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, - CONF_DEVICES, - CONF_PASSWORD, - CONF_USERNAME, - TEMP_CELSIUS, ) -from homeassistant.setup import async_setup_component +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry -from .common import load_mock_device +from .common import ( + ENTITY_NAME, + NAME, + SERIAL, + async_get_basic_device, + async_update_device, +) -from tests.async_mock import Mock, call, patch +ENTITY_ID = f"{PLATFORM_DOMAIN}.{ENTITY_NAME}" -class MockDysonState(DysonPureHotCoolState): - """Mock Dyson state.""" - - # pylint: disable=super-init-not-called - - def __init__(self): - """Create new Mock Dyson State.""" - - def __repr__(self): - """Mock repr because original one fails since constructor not called.""" - return "" - - -def _get_config(): - """Return a config dictionary.""" - return { - DYSON_DOMAIN: { - CONF_USERNAME: "email", - CONF_PASSWORD: "password", - CONF_LANGUAGE: "GB", - CONF_DEVICES: [ - {"device_id": "XX-XXXXX-XX", "device_ip": "192.168.0.1"}, - {"device_id": "YY-YYYYY-YY", "device_ip": "192.168.0.2"}, - ], - } - } - - -def _get_dyson_purehotcool_device(): - """Return a valid device as provided by the Dyson web services.""" - device = Mock(spec=DysonPureHotCool) - load_mock_device(device) - device.name = "Living room" - device.state.heat_target = "0000" - device.state.heat_mode = HeatMode.HEAT_OFF.value - device.state.fan_power = FanPower.POWER_OFF.value - device.environmental_state.humidity = 42 - device.environmental_state.temperature = 298 +@callback +def async_get_device(spec: Type[DysonDevice]) -> DysonDevice: + """Return a Dyson climate device.""" + device = async_get_basic_device(spec) + device.state.heat_target = 2900 + device.environmental_state.temperature = 275 + device.environmental_state.humidity = 50 + if spec == DysonPureHotCoolLink: + device.state.heat_mode = HeatMode.HEAT_ON.value + device.state.heat_state = HeatState.HEAT_STATE_ON.value + device.state.focus_mode = FocusMode.FOCUS_ON.value + else: + device.state.fan_power = FanPower.POWER_ON.value + device.state.heat_mode = HeatMode.HEAT_ON.value + device.state.heat_state = HeatState.HEAT_STATE_ON.value + device.state.auto_mode = AutoMode.AUTO_ON.value + device.state.fan_state = FanState.FAN_OFF.value + device.state.speed = FanSpeed.FAN_SPEED_AUTO.value return device -def _get_device_off(): - """Return a device with state off.""" - device = Mock(spec=DysonPureHotCoolLink) - load_mock_device(device) - return device - - -def _get_device_cool(): - """Return a device with state of cooling.""" - device = Mock(spec=DysonPureHotCoolLink) - load_mock_device(device) - device.state.focus_mode = FocusMode.FOCUS_OFF.value - device.state.heat_target = HeatTarget.celsius(12) - device.state.heat_mode = HeatMode.HEAT_OFF.value - device.state.heat_state = HeatState.HEAT_STATE_OFF.value - return device - - -def _get_device_heat_on(): - """Return a device with state of heating.""" - device = Mock(spec=DysonPureHotCoolLink) - load_mock_device(device) - device.serial = "YY-YYYYY-YY" - device.state.heat_target = HeatTarget.celsius(23) - device.state.heat_mode = HeatMode.HEAT_ON.value - device.state.heat_state = HeatState.HEAT_STATE_ON.value - device.environmental_state.temperature = 289 - device.environmental_state.humidity = 53 - return device - - -@pytest.fixture(autouse=True) -def patch_platforms_fixture(): - """Only set up the climate platform for the climate tests.""" - with patch("homeassistant.components.dyson.DYSON_PLATFORMS", new=[DOMAIN]): - yield - - -@patch( - "homeassistant.components.dyson.DysonAccount.devices", - return_value=[_get_device_heat_on()], +@pytest.mark.parametrize( + "device", [DysonPureHotCoolLink, DysonPureHotCool], indirect=True ) -@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) -async def test_pure_hot_cool_link_set_mode(mocked_login, mocked_devices, hass): - """Test set climate mode.""" - await async_setup_component(hass, DYSON_DOMAIN, _get_config()) - await hass.async_block_till_done() +async def test_state_common(hass: HomeAssistant, device: DysonDevice) -> None: + """Test common state and attributes of two types of climate entities.""" + er = await entity_registry.async_get_registry(hass) + assert er.async_get(ENTITY_ID).unique_id == SERIAL - device = mocked_devices.return_value[0] - - await hass.services.async_call( - DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: "climate.temp_name", ATTR_HVAC_MODE: HVAC_MODE_HEAT}, - True, - ) - - set_config = device.set_configuration - assert set_config.call_args == call(heat_mode=HeatMode.HEAT_ON) - - await hass.services.async_call( - DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: "climate.temp_name", ATTR_HVAC_MODE: HVAC_MODE_COOL}, - True, - ) - - set_config = device.set_configuration - assert set_config.call_args == call(heat_mode=HeatMode.HEAT_OFF) - - -@patch( - "homeassistant.components.dyson.DysonAccount.devices", - return_value=[_get_device_heat_on()], -) -@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) -async def test_pure_hot_cool_link_set_fan(mocked_login, mocked_devices, hass): - """Test set climate fan.""" - await async_setup_component(hass, DYSON_DOMAIN, _get_config()) - await hass.async_block_till_done() - - device = mocked_devices.return_value[0] - device.temp_unit = TEMP_CELSIUS - - await hass.services.async_call( - DOMAIN, - SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: "climate.temp_name", ATTR_FAN_MODE: FAN_FOCUS}, - True, - ) - - set_config = device.set_configuration - assert set_config.call_args == call(focus_mode=FocusMode.FOCUS_ON) - - await hass.services.async_call( - DOMAIN, - SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: "climate.temp_name", ATTR_FAN_MODE: FAN_DIFFUSE}, - True, - ) - - set_config = device.set_configuration - assert set_config.call_args == call(focus_mode=FocusMode.FOCUS_OFF) - - -@patch( - "homeassistant.components.dyson.DysonAccount.devices", - return_value=[_get_device_heat_on()], -) -@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) -async def test_pure_hot_cool_link_state(mocked_login, mocked_devices, hass): - """Test set climate temperature.""" - await async_setup_component(hass, DYSON_DOMAIN, _get_config()) - await hass.async_block_till_done() - - state = hass.states.get("climate.temp_name") - assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_FLAGS - assert state.attributes[ATTR_TEMPERATURE] == 23 - assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 289 - 273 - assert state.attributes[ATTR_CURRENT_HUMIDITY] == 53 - assert state.state == HVAC_MODE_HEAT - assert len(state.attributes[ATTR_HVAC_MODES]) == 2 - assert HVAC_MODE_HEAT in state.attributes[ATTR_HVAC_MODES] - assert HVAC_MODE_COOL in state.attributes[ATTR_HVAC_MODES] - assert len(state.attributes[ATTR_FAN_MODES]) == 2 - assert FAN_FOCUS in state.attributes[ATTR_FAN_MODES] - assert FAN_DIFFUSE in state.attributes[ATTR_FAN_MODES] - - device = mocked_devices.return_value[0] - update_callback = device.add_message_listener.call_args[0][0] - - device.state.focus_mode = FocusMode.FOCUS_ON.value - await hass.async_add_executor_job(update_callback, MockDysonState()) - await hass.async_block_till_done() - - state = hass.states.get("climate.temp_name") - assert state.attributes[ATTR_FAN_MODE] == FAN_FOCUS - - device.state.focus_mode = FocusMode.FOCUS_OFF.value - await hass.async_add_executor_job(update_callback, MockDysonState()) - await hass.async_block_till_done() - - state = hass.states.get("climate.temp_name") - assert state.attributes[ATTR_FAN_MODE] == FAN_DIFFUSE - - device.state.heat_mode = HeatMode.HEAT_ON.value - device.state.heat_state = HeatState.HEAT_STATE_OFF.value - await hass.async_add_executor_job(update_callback, MockDysonState()) - await hass.async_block_till_done() - - state = hass.states.get("climate.temp_name") - assert state.state == HVAC_MODE_HEAT - assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE - - device.environmental_state.humidity = 0 - await hass.async_add_executor_job(update_callback, MockDysonState()) - await hass.async_block_till_done() - - state = hass.states.get("climate.temp_name") - assert state.attributes.get(ATTR_CURRENT_HUMIDITY) is None - - device.environmental_state = None - await hass.async_add_executor_job(update_callback, MockDysonState()) - await hass.async_block_till_done() - - state = hass.states.get("climate.temp_name") - assert state.attributes.get(ATTR_CURRENT_HUMIDITY) is None - - device.state.heat_mode = HeatMode.HEAT_OFF.value - device.state.heat_state = HeatState.HEAT_STATE_OFF.value - await hass.async_add_executor_job(update_callback, MockDysonState()) - await hass.async_block_till_done() - - state = hass.states.get("climate.temp_name") - assert state.state == HVAC_MODE_COOL - assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL - - -@patch( - "homeassistant.components.dyson.DysonAccount.devices", - return_value=[], -) -@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) -async def test_setup_component_without_devices(mocked_login, mocked_devices, hass): - """Test setup component with no devices.""" - await async_setup_component(hass, DYSON_DOMAIN, _get_config()) - await hass.async_block_till_done() - - entity_ids = hass.states.async_entity_ids(DOMAIN) - assert not entity_ids - - -@patch( - "homeassistant.components.dyson.DysonAccount.devices", - return_value=[_get_device_heat_on()], -) -@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) -async def test_dyson_set_temperature(mocked_login, mocked_devices, hass): - """Test set climate temperature.""" - await async_setup_component(hass, DYSON_DOMAIN, _get_config()) - await hass.async_block_till_done() - - device = mocked_devices.return_value[0] - device.temp_unit = TEMP_CELSIUS - - # Without correct target temp. - await hass.services.async_call( - DOMAIN, - SERVICE_SET_TEMPERATURE, - { - ATTR_ENTITY_ID: "climate.temp_name", - ATTR_TARGET_TEMP_HIGH: 25.0, - ATTR_TARGET_TEMP_LOW: 15.0, - }, - True, - ) - - set_config = device.set_configuration - assert set_config.call_count == 0 - - await hass.services.async_call( - DOMAIN, - SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: "climate.temp_name", ATTR_TEMPERATURE: 23}, - True, - ) - - set_config = device.set_configuration - assert set_config.call_args == call( - heat_mode=HeatMode.HEAT_ON, heat_target=HeatTarget.celsius(23) - ) - - # Should clip the target temperature between 1 and 37 inclusive. - await hass.services.async_call( - DOMAIN, - SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: "climate.temp_name", ATTR_TEMPERATURE: 50}, - True, - ) - - set_config = device.set_configuration - assert set_config.call_args == call( - heat_mode=HeatMode.HEAT_ON, heat_target=HeatTarget.celsius(37) - ) - - await hass.services.async_call( - DOMAIN, - SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: "climate.temp_name", ATTR_TEMPERATURE: -5}, - True, - ) - - set_config = device.set_configuration - assert set_config.call_args == call( - heat_mode=HeatMode.HEAT_ON, heat_target=HeatTarget.celsius(1) - ) - - -@patch( - "homeassistant.components.dyson.DysonAccount.devices", - return_value=[_get_device_cool()], -) -@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) -async def test_dyson_set_temperature_when_cooling_mode( - mocked_login, mocked_devices, hass -): - """Test set climate temperature when heating is off.""" - await async_setup_component(hass, DYSON_DOMAIN, _get_config()) - await hass.async_block_till_done() - - device = mocked_devices.return_value[0] - device.temp_unit = TEMP_CELSIUS - - await hass.services.async_call( - DOMAIN, - SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: "climate.temp_name", ATTR_TEMPERATURE: 23}, - True, - ) - - set_config = device.set_configuration - assert set_config.call_args == call( - heat_mode=HeatMode.HEAT_ON, heat_target=HeatTarget.celsius(23) - ) - - -@patch( - "homeassistant.components.dyson.DysonAccount.devices", - return_value=[_get_device_heat_on(), _get_device_cool()], -) -@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) -async def test_setup_component_with_parent_discovery( - mocked_login, mocked_devices, hass -): - """Test setup_component using discovery.""" - await async_setup_component(hass, DYSON_DOMAIN, _get_config()) - await hass.async_block_till_done() - - entity_ids = hass.states.async_entity_ids(DOMAIN) - assert len(entity_ids) == 2 - - -@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) -@patch( - "homeassistant.components.dyson.DysonAccount.devices", - return_value=[_get_dyson_purehotcool_device()], -) -async def test_purehotcool_component_setup_only_once(devices, login, hass): - """Test if entities are created only once.""" - config = _get_config() - await async_setup_component(hass, DYSON_DOMAIN, config) - await hass.async_block_till_done() - - entity_ids = hass.states.async_entity_ids(DOMAIN) - assert len(entity_ids) == 1 - state = hass.states.get(entity_ids[0]) - assert state.name == "Living room" - - -@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) -@patch( - "homeassistant.components.dyson.DysonAccount.devices", - return_value=[_get_device_off()], -) -async def test_purehotcoollink_component_setup_only_once(devices, login, hass): - """Test if entities are created only once.""" - config = _get_config() - await async_setup_component(hass, DYSON_DOMAIN, config) - await hass.async_block_till_done() - - entity_ids = hass.states.async_entity_ids(DOMAIN) - assert len(entity_ids) == 1 - state = hass.states.get(entity_ids[0]) - assert state.name == "Temp Name" - - -@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) -@patch( - "homeassistant.components.dyson.DysonAccount.devices", - return_value=[_get_dyson_purehotcool_device()], -) -async def test_purehotcool_update_state(devices, login, hass): - """Test state update.""" - device = devices.return_value[0] - await async_setup_component(hass, DYSON_DOMAIN, _get_config()) - await hass.async_block_till_done() - event = { - "msg": "CURRENT-STATE", - "product-state": { - "fpwr": "ON", - "fdir": "OFF", - "auto": "OFF", - "oscs": "ON", - "oson": "ON", - "nmod": "OFF", - "rhtm": "ON", - "fnst": "FAN", - "ercd": "11E1", - "wacd": "NONE", - "nmdv": "0004", - "fnsp": "0002", - "bril": "0002", - "corf": "ON", - "cflr": "0085", - "hflr": "0095", - "sltm": "OFF", - "osal": "0045", - "osau": "0095", - "ancp": "CUST", - "tilt": "OK", - "hsta": "HEAT", - "hmax": "2986", - "hmod": "HEAT", - }, - } - device.state = DysonPureHotCoolV2State(json.dumps(event)) - update_callback = device.add_message_listener.call_args[0][0] - - await hass.async_add_executor_job(update_callback, device.state) - await hass.async_block_till_done() - state = hass.states.get("climate.living_room") + state = hass.states.get(ENTITY_ID) + assert state.name == NAME attributes = state.attributes + assert attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_FLAGS + assert attributes[ATTR_CURRENT_TEMPERATURE] == 2 + assert attributes[ATTR_CURRENT_HUMIDITY] == 50 + assert attributes[ATTR_TEMPERATURE] == 17 + assert attributes[ATTR_MIN_TEMP] == 1 + assert attributes[ATTR_MAX_TEMP] == 37 - assert attributes[ATTR_TEMPERATURE] == 25 - assert attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT - - -@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) -@patch( - "homeassistant.components.dyson.DysonAccount.devices", - return_value=[_get_dyson_purehotcool_device()], -) -async def test_purehotcool_empty_env_attributes(devices, login, hass): - """Test empty environmental state update.""" - device = devices.return_value[0] + device.state.heat_target = 2800 device.environmental_state.temperature = 0 - device.environmental_state.humidity = None - await async_setup_component(hass, DYSON_DOMAIN, _get_config()) - await hass.async_block_till_done() - - state = hass.states.get("climate.living_room") - attributes = state.attributes - + device.environmental_state.humidity = 0 + await async_update_device( + hass, + device, + DysonPureHotCoolState + if isinstance(device, DysonPureHotCoolLink) + else DysonPureHotCoolV2State, + ) + attributes = hass.states.get(ENTITY_ID).attributes + assert attributes[ATTR_CURRENT_TEMPERATURE] is None assert ATTR_CURRENT_HUMIDITY not in attributes + assert attributes[ATTR_TEMPERATURE] == 7 -@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) -@patch( - "homeassistant.components.dyson.DysonAccount.devices", - return_value=[_get_dyson_purehotcool_device()], -) -async def test_purehotcool_fan_state_off(devices, login, hass): - """Test device fan state off.""" - device = devices.return_value[0] - device.state.fan_state = FanState.FAN_OFF.value - await async_setup_component(hass, DYSON_DOMAIN, _get_config()) - await hass.async_block_till_done() - - state = hass.states.get("climate.living_room") +@pytest.mark.parametrize("device", [DysonPureHotCoolLink], indirect=True) +async def test_state_purehotcoollink( + hass: HomeAssistant, device: DysonPureHotCoolLink +) -> None: + """Test common state and attributes of a PureHotCoolLink entity.""" + state = hass.states.get(ENTITY_ID) + assert state.state == HVAC_MODE_HEAT attributes = state.attributes + assert attributes[ATTR_HVAC_MODES] == SUPPORT_HVAC + assert attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT + assert attributes[ATTR_FAN_MODE] == FAN_FOCUS + assert attributes[ATTR_FAN_MODES] == SUPPORT_FAN - assert attributes[ATTR_FAN_MODE] == FAN_OFF - - -@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) -@patch( - "homeassistant.components.dyson.DysonAccount.devices", - return_value=[_get_dyson_purehotcool_device()], -) -async def test_purehotcool_hvac_action_cool(devices, login, hass): - """Test device HVAC action cool.""" - device = devices.return_value[0] - device.state.fan_power = FanPower.POWER_ON.value - await async_setup_component(hass, DYSON_DOMAIN, _get_config()) - await hass.async_block_till_done() - - state = hass.states.get("climate.living_room") + device.state.heat_state = HeatState.HEAT_STATE_OFF.value + device.state.focus_mode = FocusMode.FOCUS_OFF + await async_update_device(hass, device, DysonPureHotCoolState) + state = hass.states.get(ENTITY_ID) + assert state.state == HVAC_MODE_HEAT attributes = state.attributes + assert attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + assert attributes[ATTR_FAN_MODE] == FAN_DIFFUSE + device.state.heat_mode = HeatMode.HEAT_OFF.value + await async_update_device(hass, device, DysonPureHotCoolState) + state = hass.states.get(ENTITY_ID) + assert state.state == HVAC_MODE_COOL + attributes = state.attributes assert attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL -@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) -@patch( - "homeassistant.components.dyson.DysonAccount.devices", - return_value=[_get_dyson_purehotcool_device()], -) -async def test_purehotcool_hvac_action_idle(devices, login, hass): - """Test device HVAC action idle.""" - device = devices.return_value[0] - device.state.fan_power = FanPower.POWER_ON.value - device.state.heat_mode = HeatMode.HEAT_ON.value - await async_setup_component(hass, DYSON_DOMAIN, _get_config()) - await hass.async_block_till_done() - - state = hass.states.get("climate.living_room") +@pytest.mark.parametrize("device", [DysonPureHotCool], indirect=True) +async def test_state_purehotcool(hass: HomeAssistant, device: DysonPureHotCool) -> None: + """Test common state and attributes of a PureHotCool entity.""" + state = hass.states.get(ENTITY_ID) + assert state.state == HVAC_MODE_HEAT attributes = state.attributes + assert attributes[ATTR_HVAC_MODES] == SUPPORT_HVAC_PCOOL + assert attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT + assert attributes[ATTR_FAN_MODE] == FAN_AUTO + assert attributes[ATTR_FAN_MODES] == SUPPORT_FAN_PCOOL + device.state.heat_state = HeatState.HEAT_STATE_OFF.value + device.state.auto_mode = AutoMode.AUTO_OFF.value + await async_update_device(hass, device, DysonPureHotCoolV2State) + state = hass.states.get(ENTITY_ID) + assert state.state == HVAC_MODE_HEAT + attributes = state.attributes assert attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + assert attributes[ATTR_FAN_MODE] == FAN_OFF - -@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) -@patch( - "homeassistant.components.dyson.DysonAccount.devices", - return_value=[_get_dyson_purehotcool_device()], -) -async def test_purehotcool_set_temperature(devices, login, hass): - """Test set temperature.""" - device = devices.return_value[0] - await async_setup_component(hass, DYSON_DOMAIN, _get_config()) - await hass.async_block_till_done() - state = hass.states.get("climate.living_room") + device.state.heat_mode = HeatMode.HEAT_OFF.value + device.state.fan_state = FanState.FAN_ON.value + device.state.speed = FanSpeed.FAN_SPEED_1.value + await async_update_device(hass, device, DysonPureHotCoolV2State) + state = hass.states.get(ENTITY_ID) + assert state.state == HVAC_MODE_COOL attributes = state.attributes - min_temp = attributes[ATTR_MIN_TEMP] - max_temp = attributes[ATTR_MAX_TEMP] + assert attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_COOL + assert attributes[ATTR_FAN_MODE] == FAN_LOW - await hass.services.async_call( - DOMAIN, - SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: "climate.bed_room", ATTR_TEMPERATURE: 23}, - True, - ) - device.set_heat_target.assert_not_called() - - await hass.services.async_call( - DOMAIN, - SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: "climate.living_room", ATTR_TEMPERATURE: 23}, - True, - ) - assert device.set_heat_target.call_count == 1 - device.set_heat_target.assert_called_with("2960") - - await hass.services.async_call( - DOMAIN, - SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: "climate.living_room", ATTR_TEMPERATURE: min_temp - 1}, - True, - ) - assert device.set_heat_target.call_count == 2 - device.set_heat_target.assert_called_with(HeatTarget.celsius(min_temp)) - - await hass.services.async_call( - DOMAIN, - SERVICE_SET_TEMPERATURE, - {ATTR_ENTITY_ID: "climate.living_room", ATTR_TEMPERATURE: max_temp + 1}, - True, - ) - assert device.set_heat_target.call_count == 3 - device.set_heat_target.assert_called_with(HeatTarget.celsius(max_temp)) + device.state.fan_power = FanPower.POWER_OFF.value + await async_update_device(hass, device, DysonPureHotCoolV2State) + state = hass.states.get(ENTITY_ID) + assert state.state == HVAC_MODE_OFF + attributes = state.attributes + assert attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF -@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) -@patch( - "homeassistant.components.dyson.DysonAccount.devices", - return_value=[_get_dyson_purehotcool_device()], +@pytest.mark.parametrize( + "service,service_data,configuration_data", + [ + ( + SERVICE_SET_TEMPERATURE, + {ATTR_TEMPERATURE: -5}, + {"heat_target": "2740", "heat_mode": HeatMode.HEAT_ON}, + ), + ( + SERVICE_SET_TEMPERATURE, + {ATTR_TEMPERATURE: 40}, + {"heat_target": "3100", "heat_mode": HeatMode.HEAT_ON}, + ), + ( + SERVICE_SET_TEMPERATURE, + {ATTR_TEMPERATURE: 20}, + {"heat_target": "2930", "heat_mode": HeatMode.HEAT_ON}, + ), + ( + SERVICE_SET_FAN_MODE, + {ATTR_FAN_MODE: FAN_FOCUS}, + {"focus_mode": FocusMode.FOCUS_ON}, + ), + ( + SERVICE_SET_FAN_MODE, + {ATTR_FAN_MODE: FAN_DIFFUSE}, + {"focus_mode": FocusMode.FOCUS_OFF}, + ), + ( + SERVICE_SET_HVAC_MODE, + {ATTR_HVAC_MODE: HVAC_MODE_HEAT}, + {"heat_mode": HeatMode.HEAT_ON}, + ), + ( + SERVICE_SET_HVAC_MODE, + {ATTR_HVAC_MODE: HVAC_MODE_COOL}, + {"heat_mode": HeatMode.HEAT_OFF}, + ), + ], ) -async def test_purehotcool_set_fan_mode(devices, login, hass): - """Test set fan mode.""" - device = devices.return_value[0] - await async_setup_component(hass, DYSON_DOMAIN, _get_config()) - await hass.async_block_till_done() - +@pytest.mark.parametrize("device", [DysonPureHotCoolLink], indirect=True) +async def test_commands_purehotcoollink( + hass: HomeAssistant, + device: DysonPureHotCoolLink, + service: str, + service_data: dict, + configuration_data: dict, +) -> None: + """Test sending commands to a PureHotCoolLink entity.""" await hass.services.async_call( - DOMAIN, - SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: "climate.bed_room", ATTR_FAN_MODE: FAN_OFF}, - True, + PLATFORM_DOMAIN, + service, + { + ATTR_ENTITY_ID: ENTITY_ID, + **service_data, + }, + blocking=True, ) - device.turn_off.assert_not_called() - - await hass.services.async_call( - DOMAIN, - SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: "climate.living_room", ATTR_FAN_MODE: FAN_OFF}, - True, - ) - assert device.turn_off.call_count == 1 - - await hass.services.async_call( - DOMAIN, - SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: "climate.living_room", ATTR_FAN_MODE: FAN_LOW}, - True, - ) - assert device.set_fan_speed.call_count == 1 - device.set_fan_speed.assert_called_with(FanSpeed.FAN_SPEED_4) - - await hass.services.async_call( - DOMAIN, - SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: "climate.living_room", ATTR_FAN_MODE: FAN_MEDIUM}, - True, - ) - assert device.set_fan_speed.call_count == 2 - device.set_fan_speed.assert_called_with(FanSpeed.FAN_SPEED_7) - - await hass.services.async_call( - DOMAIN, - SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: "climate.living_room", ATTR_FAN_MODE: FAN_HIGH}, - True, - ) - assert device.set_fan_speed.call_count == 3 - device.set_fan_speed.assert_called_with(FanSpeed.FAN_SPEED_10) - - await hass.services.async_call( - DOMAIN, - SERVICE_SET_FAN_MODE, - {ATTR_ENTITY_ID: "climate.living_room", ATTR_FAN_MODE: FAN_AUTO}, - True, - ) - assert device.enable_auto_mode.call_count == 1 + device.set_configuration.assert_called_once_with(**configuration_data) -@patch("homeassistant.components.dyson.DysonAccount.login", return_value=True) -@patch( - "homeassistant.components.dyson.DysonAccount.devices", - return_value=[_get_dyson_purehotcool_device()], +@pytest.mark.parametrize( + "service,service_data,command,command_args", + [ + (SERVICE_SET_TEMPERATURE, {ATTR_TEMPERATURE: 20}, "set_heat_target", ["2930"]), + (SERVICE_SET_FAN_MODE, {ATTR_FAN_MODE: FAN_OFF}, "turn_off", []), + ( + SERVICE_SET_FAN_MODE, + {ATTR_FAN_MODE: FAN_LOW}, + "set_fan_speed", + [FanSpeed.FAN_SPEED_4], + ), + ( + SERVICE_SET_FAN_MODE, + {ATTR_FAN_MODE: FAN_MEDIUM}, + "set_fan_speed", + [FanSpeed.FAN_SPEED_7], + ), + ( + SERVICE_SET_FAN_MODE, + {ATTR_FAN_MODE: FAN_HIGH}, + "set_fan_speed", + [FanSpeed.FAN_SPEED_10], + ), + (SERVICE_SET_FAN_MODE, {ATTR_FAN_MODE: FAN_AUTO}, "enable_auto_mode", []), + (SERVICE_SET_HVAC_MODE, {ATTR_HVAC_MODE: HVAC_MODE_OFF}, "turn_off", []), + ( + SERVICE_SET_HVAC_MODE, + {ATTR_HVAC_MODE: HVAC_MODE_HEAT}, + "enable_heat_mode", + [], + ), + ( + SERVICE_SET_HVAC_MODE, + {ATTR_HVAC_MODE: HVAC_MODE_COOL}, + "disable_heat_mode", + [], + ), + ], ) -async def test_purehotcool_set_hvac_mode(devices, login, hass): - """Test set HVAC mode.""" - device = devices.return_value[0] - await async_setup_component(hass, DYSON_DOMAIN, _get_config()) - await hass.async_block_till_done() - +@pytest.mark.parametrize("device", [DysonPureHotCool], indirect=True) +async def test_commands_purehotcool( + hass: HomeAssistant, + device: DysonPureHotCoolLink, + service: str, + service_data: dict, + command: str, + command_args: list, +) -> None: + """Test sending commands to a PureHotCool entity.""" await hass.services.async_call( - DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: "climate.bed_room", ATTR_HVAC_MODE: HVAC_MODE_OFF}, - True, + PLATFORM_DOMAIN, + service, + { + ATTR_ENTITY_ID: ENTITY_ID, + **service_data, + }, + blocking=True, ) - device.turn_off.assert_not_called() + getattr(device, command).assert_called_once_with(*command_args) - await hass.services.async_call( - DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: "climate.living_room", ATTR_HVAC_MODE: HVAC_MODE_OFF}, - True, - ) - assert device.turn_off.call_count == 1 +@pytest.mark.parametrize("hvac_mode", [HVAC_MODE_HEAT, HVAC_MODE_COOL]) +@pytest.mark.parametrize( + "fan_power,turn_on_call_count", + [ + (FanPower.POWER_ON.value, 0), + (FanPower.POWER_OFF.value, 1), + ], +) +@pytest.mark.parametrize("device", [DysonPureHotCool], indirect=True) +async def test_set_hvac_mode_purehotcool( + hass: HomeAssistant, + device: DysonPureHotCoolLink, + hvac_mode: str, + fan_power: str, + turn_on_call_count: int, +) -> None: + """Test setting HVAC mode of a PureHotCool entity turns on the device when it's off.""" + device.state.fan_power = fan_power + await async_update_device(hass, device) await hass.services.async_call( - DOMAIN, + PLATFORM_DOMAIN, SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: "climate.living_room", ATTR_HVAC_MODE: HVAC_MODE_HEAT}, - True, + { + ATTR_ENTITY_ID: ENTITY_ID, + ATTR_HVAC_MODE: hvac_mode, + }, + blocking=True, ) - assert device.turn_on.call_count == 1 - assert device.enable_heat_mode.call_count == 1 - - await hass.services.async_call( - DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: "climate.living_room", ATTR_HVAC_MODE: HVAC_MODE_COOL}, - True, - ) - assert device.turn_on.call_count == 2 - assert device.disable_heat_mode.call_count == 1 + assert device.turn_on.call_count == turn_on_call_count diff --git a/tests/components/dyson/test_fan.py b/tests/components/dyson/test_fan.py index 11770e1f133..310d9197133 100644 --- a/tests/components/dyson/test_fan.py +++ b/tests/components/dyson/test_fan.py @@ -1,894 +1,400 @@ """Test the Dyson fan component.""" -import json -import unittest -from unittest import mock +from typing import Type from libpurecool.const import FanMode, FanSpeed, NightMode, Oscillation -from libpurecool.dyson_pure_cool import DysonPureCool -from libpurecool.dyson_pure_cool_link import DysonPureCoolLink +from libpurecool.dyson_pure_cool import DysonPureCool, DysonPureCoolLink from libpurecool.dyson_pure_state import DysonPureCoolState from libpurecool.dyson_pure_state_v2 import DysonPureCoolV2State +import pytest -from homeassistant.components import dyson as dyson_parent -from homeassistant.components.dyson import DYSON_DEVICES -import homeassistant.components.dyson.fan as dyson +from homeassistant.components.dyson import DOMAIN +from homeassistant.components.dyson.fan import ( + ATTR_ANGLE_HIGH, + ATTR_ANGLE_LOW, + ATTR_AUTO_MODE, + ATTR_CARBON_FILTER, + ATTR_DYSON_SPEED, + ATTR_DYSON_SPEED_LIST, + ATTR_FLOW_DIRECTION_FRONT, + ATTR_HEPA_FILTER, + ATTR_NIGHT_MODE, + ATTR_TIMER, + SERVICE_SET_ANGLE, + SERVICE_SET_AUTO_MODE, + SERVICE_SET_DYSON_SPEED, + SERVICE_SET_FLOW_DIRECTION_FRONT, + SERVICE_SET_NIGHT_MODE, + SERVICE_SET_TIMER, + SPEED_LOW, +) from homeassistant.components.fan import ( ATTR_OSCILLATING, ATTR_SPEED, - DOMAIN, + ATTR_SPEED_LIST, + DOMAIN as PLATFORM_DOMAIN, SERVICE_OSCILLATE, + SERVICE_SET_SPEED, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, SPEED_HIGH, - SPEED_LOW, SPEED_MEDIUM, + SUPPORT_OSCILLATE, + SUPPORT_SET_SPEED, ) -from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON -from homeassistant.helpers import discovery -from homeassistant.setup import async_setup_component - -from .common import load_mock_device - -from tests.async_mock import patch -from tests.common import get_test_home_assistant - - -class MockDysonState(DysonPureCoolState): - """Mock Dyson state.""" - - def __init__(self): - """Create new Mock Dyson State.""" - pass - - def __repr__(self): - """Mock repr because original one fails since constructor not called.""" - return "" - - -def _get_dyson_purecool_device(): - """Return a valid device as provided by the Dyson web services.""" - device = mock.Mock(spec=DysonPureCool) - load_mock_device(device) - device.name = "Living room" - return device - - -def _get_dyson_purecoollink_device(): - """Return a valid device as provided by the Dyson web services.""" - device = mock.Mock(spec=DysonPureCoolLink) - load_mock_device(device) - device.name = "Living room" - device.state.oscillation = "ON" - device.state.fan_mode = "FAN" - device.state.speed = FanSpeed.FAN_SPEED_AUTO.value - return device - - -def _get_supported_speeds(): - return [ - int(FanSpeed.FAN_SPEED_1.value), - int(FanSpeed.FAN_SPEED_2.value), - int(FanSpeed.FAN_SPEED_3.value), - int(FanSpeed.FAN_SPEED_4.value), - int(FanSpeed.FAN_SPEED_5.value), - int(FanSpeed.FAN_SPEED_6.value), - int(FanSpeed.FAN_SPEED_7.value), - int(FanSpeed.FAN_SPEED_8.value), - int(FanSpeed.FAN_SPEED_9.value), - int(FanSpeed.FAN_SPEED_10.value), - ] - - -def _get_config(): - """Return a config dictionary.""" - return { - dyson_parent.DOMAIN: { - dyson_parent.CONF_USERNAME: "email", - dyson_parent.CONF_PASSWORD: "password", - dyson_parent.CONF_LANGUAGE: "GB", - dyson_parent.CONF_DEVICES: [ - {"device_id": "XX-XXXXX-XX", "device_ip": "192.168.0.1"} - ], - } - } - - -def _get_device_with_no_state(): - """Return a device with no state.""" - device = mock.Mock() - device.name = "Device_name" - device.state = None - return device - - -def _get_device_off(): - """Return a device with state off.""" - device = mock.Mock() - device.name = "Device_name" - device.state = mock.Mock() - device.state.fan_mode = "OFF" - device.state.night_mode = "ON" - device.state.speed = "0004" - return device - - -def _get_device_auto(): - """Return a device with state auto.""" - device = mock.Mock() - device.name = "Device_name" - device.state = mock.Mock() - device.state.fan_mode = "AUTO" - device.state.night_mode = "ON" - device.state.speed = "AUTO" - return device - - -def _get_device_on(): - """Return a valid state on.""" - device = mock.Mock(spec=DysonPureCoolLink) - device.name = "Device_name" - device.state = mock.Mock() - device.state.fan_mode = "FAN" - device.state.fan_state = "FAN" - device.state.oscillation = "ON" - device.state.night_mode = "OFF" - device.state.speed = "0001" - return device - - -class DysonSetupTest(unittest.TestCase): - """Dyson component setup tests.""" - - def setUp(self): # pylint: disable=invalid-name - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.addCleanup(self.tear_down_cleanup) - - def tear_down_cleanup(self): - """Stop everything that was started.""" - self.hass.stop() - - def test_setup_component_with_no_devices(self): - """Test setup component with no devices.""" - self.hass.data[dyson.DYSON_DEVICES] = [] - add_entities = mock.MagicMock() - dyson.setup_platform(self.hass, None, add_entities, mock.Mock()) - add_entities.assert_called_with([]) - - def test_setup_component(self): - """Test setup component with devices.""" - - def _add_device(devices): - assert len(devices) == 2 - assert devices[0].name == "Device_name" - - device_fan = _get_device_on() - device_purecool_fan = _get_dyson_purecool_device() - device_non_fan = _get_device_off() - - self.hass.data[dyson.DYSON_DEVICES] = [ - device_fan, - device_purecool_fan, - device_non_fan, - ] - dyson.setup_platform(self.hass, None, _add_device) - - -class DysonTest(unittest.TestCase): - """Dyson fan component test class.""" - - def setUp(self): # pylint: disable=invalid-name - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.addCleanup(self.tear_down_cleanup) - - def tear_down_cleanup(self): - """Stop everything that was started.""" - self.hass.stop() - - def test_dyson_set_speed(self): - """Test set fan speed.""" - device = _get_device_on() - component = dyson.DysonPureCoolLinkDevice(self.hass, device) - assert not component.should_poll - component.set_speed("1") - set_config = device.set_configuration - set_config.assert_called_with( - fan_mode=FanMode.FAN, fan_speed=FanSpeed.FAN_SPEED_1 - ) - - component.set_speed("AUTO") - set_config = device.set_configuration - set_config.assert_called_with(fan_mode=FanMode.AUTO) - - def test_dyson_turn_on(self): - """Test turn on fan.""" - device = _get_device_on() - component = dyson.DysonPureCoolLinkDevice(self.hass, device) - assert not component.should_poll - component.turn_on() - set_config = device.set_configuration - set_config.assert_called_with(fan_mode=FanMode.FAN) - - def test_dyson_turn_night_mode(self): - """Test turn on fan with night mode.""" - device = _get_device_on() - component = dyson.DysonPureCoolLinkDevice(self.hass, device) - assert not component.should_poll - component.set_night_mode(True) - set_config = device.set_configuration - set_config.assert_called_with(night_mode=NightMode.NIGHT_MODE_ON) - - component.set_night_mode(False) - set_config = device.set_configuration - set_config.assert_called_with(night_mode=NightMode.NIGHT_MODE_OFF) - - def test_is_night_mode(self): - """Test night mode.""" - device = _get_device_on() - component = dyson.DysonPureCoolLinkDevice(self.hass, device) - assert not component.night_mode - - device = _get_device_off() - component = dyson.DysonPureCoolLinkDevice(self.hass, device) - assert component.night_mode - - def test_dyson_turn_auto_mode(self): - """Test turn on/off fan with auto mode.""" - device = _get_device_on() - component = dyson.DysonPureCoolLinkDevice(self.hass, device) - assert not component.should_poll - component.set_auto_mode(True) - set_config = device.set_configuration - set_config.assert_called_with(fan_mode=FanMode.AUTO) - - component.set_auto_mode(False) - set_config = device.set_configuration - set_config.assert_called_with(fan_mode=FanMode.FAN) - - def test_is_auto_mode(self): - """Test auto mode.""" - device = _get_device_on() - component = dyson.DysonPureCoolLinkDevice(self.hass, device) - assert not component.auto_mode - - device = _get_device_auto() - component = dyson.DysonPureCoolLinkDevice(self.hass, device) - assert component.auto_mode - - def test_dyson_turn_on_speed(self): - """Test turn on fan with specified speed.""" - device = _get_device_on() - component = dyson.DysonPureCoolLinkDevice(self.hass, device) - assert not component.should_poll - component.turn_on("1") - set_config = device.set_configuration - set_config.assert_called_with( - fan_mode=FanMode.FAN, fan_speed=FanSpeed.FAN_SPEED_1 - ) - - component.turn_on("AUTO") - set_config = device.set_configuration - set_config.assert_called_with(fan_mode=FanMode.AUTO) - - def test_dyson_turn_off(self): - """Test turn off fan.""" - device = _get_device_on() - component = dyson.DysonPureCoolLinkDevice(self.hass, device) - assert not component.should_poll - component.turn_off() - set_config = device.set_configuration - set_config.assert_called_with(fan_mode=FanMode.OFF) - - def test_dyson_oscillate_off(self): - """Test turn off oscillation.""" - device = _get_device_on() - component = dyson.DysonPureCoolLinkDevice(self.hass, device) - component.oscillate(False) - set_config = device.set_configuration - set_config.assert_called_with(oscillation=Oscillation.OSCILLATION_OFF) - - def test_dyson_oscillate_on(self): - """Test turn on oscillation.""" - device = _get_device_on() - component = dyson.DysonPureCoolLinkDevice(self.hass, device) - component.oscillate(True) - set_config = device.set_configuration - set_config.assert_called_with(oscillation=Oscillation.OSCILLATION_ON) - - def test_dyson_oscillate_value_on(self): - """Test get oscillation value on.""" - device = _get_device_on() - component = dyson.DysonPureCoolLinkDevice(self.hass, device) - assert component.oscillating - - def test_dyson_oscillate_value_off(self): - """Test get oscillation value off.""" - device = _get_device_off() - component = dyson.DysonPureCoolLinkDevice(self.hass, device) - assert not component.oscillating - - def test_dyson_on(self): - """Test device is on.""" - device = _get_device_on() - component = dyson.DysonPureCoolLinkDevice(self.hass, device) - assert component.is_on - - def test_dyson_off(self): - """Test device is off.""" - device = _get_device_off() - component = dyson.DysonPureCoolLinkDevice(self.hass, device) - assert not component.is_on - - device = _get_device_with_no_state() - component = dyson.DysonPureCoolLinkDevice(self.hass, device) - assert not component.is_on - - def test_dyson_get_speed(self): - """Test get device speed.""" - device = _get_device_on() - component = dyson.DysonPureCoolLinkDevice(self.hass, device) - assert component.speed == 1 - - device = _get_device_off() - component = dyson.DysonPureCoolLinkDevice(self.hass, device) - assert component.speed == 4 - - device = _get_device_with_no_state() - component = dyson.DysonPureCoolLinkDevice(self.hass, device) - assert component.speed is None - - device = _get_device_auto() - component = dyson.DysonPureCoolLinkDevice(self.hass, device) - assert component.speed == "AUTO" - - def test_dyson_get_direction(self): - """Test get device direction.""" - device = _get_device_on() - component = dyson.DysonPureCoolLinkDevice(self.hass, device) - assert component.current_direction is None - - def test_dyson_get_speed_list(self): - """Test get speeds list.""" - device = _get_device_on() - component = dyson.DysonPureCoolLinkDevice(self.hass, device) - assert len(component.speed_list) == 11 - - def test_dyson_supported_features(self): - """Test supported features.""" - device = _get_device_on() - component = dyson.DysonPureCoolLinkDevice(self.hass, device) - assert component.supported_features == 3 - - def test_on_message(self): - """Test when message is received.""" - device = _get_device_on() - component = dyson.DysonPureCoolLinkDevice(self.hass, device) - component.entity_id = "entity_id" - component.schedule_update_ha_state = mock.Mock() - component.on_message(MockDysonState()) - component.schedule_update_ha_state.assert_called_with() - - def test_service_set_night_mode(self): - """Test set night mode service.""" - dyson_device = mock.MagicMock() - self.hass.data[DYSON_DEVICES] = [] - dyson_device.entity_id = "fan.living_room" - self.hass.data[dyson.DYSON_FAN_DEVICES] = [dyson_device] - dyson.setup_platform(self.hass, None, mock.MagicMock(), mock.MagicMock()) - - self.hass.services.call( - dyson.DYSON_DOMAIN, - dyson.SERVICE_SET_NIGHT_MODE, - {"entity_id": "fan.bed_room", "night_mode": True}, - True, - ) - assert dyson_device.set_night_mode.call_count == 0 - - self.hass.services.call( - dyson.DYSON_DOMAIN, - dyson.SERVICE_SET_NIGHT_MODE, - {"entity_id": "fan.living_room", "night_mode": True}, - True, - ) - dyson_device.set_night_mode.assert_called_with(True) - - -@patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_dyson_purecoollink_device()], +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + STATE_OFF, + STATE_ON, ) -async def test_purecoollink_attributes(devices, login, hass): - """Test state attributes.""" - await async_setup_component(hass, dyson.DYSON_DOMAIN, _get_config()) - await hass.async_block_till_done() - fan_state = hass.states.get("fan.living_room") - attributes = fan_state.attributes +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry - assert fan_state.state == "on" - assert attributes[dyson.ATTR_NIGHT_MODE] is False - assert attributes[ATTR_SPEED] == FanSpeed.FAN_SPEED_AUTO.value +from .common import ( + ENTITY_NAME, + NAME, + SERIAL, + async_get_purecool_device, + async_get_purecoollink_device, + async_update_device, +) + +ENTITY_ID = f"{PLATFORM_DOMAIN}.{ENTITY_NAME}" + + +@callback +def async_get_device(spec: Type[DysonPureCoolLink]) -> DysonPureCoolLink: + """Return a Dyson fan device.""" + if spec == DysonPureCoolLink: + return async_get_purecoollink_device() + return async_get_purecool_device() + + +@pytest.mark.parametrize("device", [DysonPureCoolLink], indirect=True) +async def test_state_purecoollink( + hass: HomeAssistant, device: DysonPureCoolLink +) -> None: + """Test the state of a PureCoolLink fan.""" + er = await entity_registry.async_get_registry(hass) + assert er.async_get(ENTITY_ID).unique_id == SERIAL + + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + assert state.name == NAME + attributes = state.attributes + assert attributes[ATTR_NIGHT_MODE] is True assert attributes[ATTR_OSCILLATING] is True + assert attributes[ATTR_SPEED] == SPEED_LOW + assert attributes[ATTR_SPEED_LIST] == [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + assert attributes[ATTR_DYSON_SPEED] == 1 + assert attributes[ATTR_DYSON_SPEED_LIST] == list(range(1, 11)) + assert attributes[ATTR_AUTO_MODE] is False + assert attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_OSCILLATE | SUPPORT_SET_SPEED + device.state.fan_mode = FanMode.OFF.value + await async_update_device(hass, device, DysonPureCoolState) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF -@patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_dyson_purecool_device()], -) -async def test_purecool_turn_on(devices, login, hass): - """Test turn on.""" - device = devices.return_value[0] - await async_setup_component(hass, dyson.DYSON_DOMAIN, _get_config()) - await hass.async_block_till_done() - - await hass.services.async_call( - DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: "fan.bed_room"}, True - ) - assert device.turn_on.call_count == 0 - - await hass.services.async_call( - DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: "fan.living_room"}, True - ) - assert device.turn_on.call_count == 1 - - -@patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_dyson_purecool_device()], -) -async def test_purecool_set_speed(devices, login, hass): - """Test set speed.""" - device = devices.return_value[0] - await async_setup_component(hass, dyson.DYSON_DOMAIN, _get_config()) - await hass.async_block_till_done() - - await hass.services.async_call( - DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.bed_room", ATTR_SPEED: SPEED_LOW}, - True, - ) - assert device.set_fan_speed.call_count == 0 - - await hass.services.async_call( - DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.living_room", ATTR_SPEED: SPEED_LOW}, - True, - ) - device.set_fan_speed.assert_called_with(FanSpeed.FAN_SPEED_4) - - await hass.services.async_call( - DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.living_room", ATTR_SPEED: SPEED_MEDIUM}, - True, - ) - device.set_fan_speed.assert_called_with(FanSpeed.FAN_SPEED_7) - - await hass.services.async_call( - DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "fan.living_room", ATTR_SPEED: SPEED_HIGH}, - True, - ) - device.set_fan_speed.assert_called_with(FanSpeed.FAN_SPEED_10) - - -@patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_dyson_purecool_device()], -) -async def test_purecool_turn_off(devices, login, hass): - """Test turn off.""" - device = devices.return_value[0] - await async_setup_component(hass, dyson.DYSON_DOMAIN, _get_config()) - await hass.async_block_till_done() - - await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "fan.bed_room"}, True - ) - assert device.turn_off.call_count == 0 - - await hass.services.async_call( - DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: "fan.living_room"}, True - ) - assert device.turn_off.call_count == 1 - - -@patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_dyson_purecool_device()], -) -async def test_purecool_set_dyson_speed(devices, login, hass): - """Test set exact dyson speed.""" - device = devices.return_value[0] - await async_setup_component(hass, dyson.DYSON_DOMAIN, _get_config()) - await hass.async_block_till_done() - - await hass.services.async_call( - dyson.DYSON_DOMAIN, - dyson.SERVICE_SET_DYSON_SPEED, - { - ATTR_ENTITY_ID: "fan.bed_room", - dyson.ATTR_DYSON_SPEED: int(FanSpeed.FAN_SPEED_2.value), - }, - True, - ) - assert device.set_fan_speed.call_count == 0 - - await hass.services.async_call( - dyson.DYSON_DOMAIN, - dyson.SERVICE_SET_DYSON_SPEED, - { - ATTR_ENTITY_ID: "fan.living_room", - dyson.ATTR_DYSON_SPEED: int(FanSpeed.FAN_SPEED_2.value), - }, - True, - ) - device.set_fan_speed.assert_called_with(FanSpeed.FAN_SPEED_2) - - -@patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_dyson_purecool_device()], -) -async def test_purecool_oscillate(devices, login, hass): - """Test set oscillation.""" - device = devices.return_value[0] - await async_setup_component(hass, dyson.DYSON_DOMAIN, _get_config()) - await hass.async_block_till_done() - - await hass.services.async_call( - DOMAIN, - SERVICE_OSCILLATE, - {ATTR_ENTITY_ID: "fan.bed_room", ATTR_OSCILLATING: True}, - True, - ) - assert device.enable_oscillation.call_count == 0 - - await hass.services.async_call( - DOMAIN, - SERVICE_OSCILLATE, - {ATTR_ENTITY_ID: "fan.living_room", ATTR_OSCILLATING: True}, - True, - ) - assert device.enable_oscillation.call_count == 1 - - await hass.services.async_call( - DOMAIN, - SERVICE_OSCILLATE, - {ATTR_ENTITY_ID: "fan.living_room", ATTR_OSCILLATING: False}, - True, - ) - assert device.disable_oscillation.call_count == 1 - - -@patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_dyson_purecool_device()], -) -async def test_purecool_set_night_mode(devices, login, hass): - """Test set night mode.""" - device = devices.return_value[0] - await async_setup_component(hass, dyson.DYSON_DOMAIN, _get_config()) - - await hass.async_block_till_done() - - await hass.services.async_call( - dyson.DYSON_DOMAIN, - dyson.SERVICE_SET_NIGHT_MODE, - {"entity_id": "fan.bed_room", "night_mode": True}, - True, - ) - assert device.enable_night_mode.call_count == 0 - - await hass.services.async_call( - dyson.DYSON_DOMAIN, - dyson.SERVICE_SET_NIGHT_MODE, - {"entity_id": "fan.living_room", "night_mode": True}, - True, - ) - assert device.enable_night_mode.call_count == 1 - - await hass.services.async_call( - dyson.DYSON_DOMAIN, - dyson.SERVICE_SET_NIGHT_MODE, - {"entity_id": "fan.living_room", "night_mode": False}, - True, - ) - assert device.disable_night_mode.call_count == 1 - - -@patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_dyson_purecool_device()], -) -async def test_purecool_set_auto_mode(devices, login, hass): - """Test set auto mode.""" - device = devices.return_value[0] - await async_setup_component(hass, dyson.DYSON_DOMAIN, _get_config()) - await hass.async_block_till_done() - - await hass.services.async_call( - dyson.DYSON_DOMAIN, - dyson.SERVICE_SET_AUTO_MODE, - {ATTR_ENTITY_ID: "fan.bed_room", dyson.ATTR_AUTO_MODE: True}, - True, - ) - assert device.enable_auto_mode.call_count == 0 - - await hass.services.async_call( - dyson.DYSON_DOMAIN, - dyson.SERVICE_SET_AUTO_MODE, - {ATTR_ENTITY_ID: "fan.living_room", dyson.ATTR_AUTO_MODE: True}, - True, - ) - assert device.enable_auto_mode.call_count == 1 - - await hass.services.async_call( - dyson.DYSON_DOMAIN, - dyson.SERVICE_SET_AUTO_MODE, - {ATTR_ENTITY_ID: "fan.living_room", dyson.ATTR_AUTO_MODE: False}, - True, - ) - assert device.disable_auto_mode.call_count == 1 - - -@patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_dyson_purecool_device()], -) -async def test_purecool_set_angle(devices, login, hass): - """Test set angle.""" - device = devices.return_value[0] - await async_setup_component(hass, dyson.DYSON_DOMAIN, _get_config()) - await hass.async_block_till_done() - - await hass.services.async_call( - dyson.DYSON_DOMAIN, - dyson.SERVICE_SET_ANGLE, - { - ATTR_ENTITY_ID: "fan.bed_room", - dyson.ATTR_ANGLE_LOW: 90, - dyson.ATTR_ANGLE_HIGH: 180, - }, - True, - ) - assert device.enable_oscillation.call_count == 0 - - await hass.services.async_call( - dyson.DYSON_DOMAIN, - dyson.SERVICE_SET_ANGLE, - { - ATTR_ENTITY_ID: "fan.living_room", - dyson.ATTR_ANGLE_LOW: 90, - dyson.ATTR_ANGLE_HIGH: 180, - }, - True, - ) - device.enable_oscillation.assert_called_with(90, 180) - - -@patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_dyson_purecool_device()], -) -async def test_purecool_set_flow_direction_front(devices, login, hass): - """Test set frontal flow direction.""" - device = devices.return_value[0] - await async_setup_component(hass, dyson.DYSON_DOMAIN, _get_config()) - await hass.async_block_till_done() - - await hass.services.async_call( - dyson.DYSON_DOMAIN, - dyson.SERVICE_SET_FLOW_DIRECTION_FRONT, - {ATTR_ENTITY_ID: "fan.bed_room", dyson.ATTR_FLOW_DIRECTION_FRONT: True}, - True, - ) - assert device.enable_frontal_direction.call_count == 0 - - await hass.services.async_call( - dyson.DYSON_DOMAIN, - dyson.SERVICE_SET_FLOW_DIRECTION_FRONT, - {ATTR_ENTITY_ID: "fan.living_room", dyson.ATTR_FLOW_DIRECTION_FRONT: True}, - True, - ) - assert device.enable_frontal_direction.call_count == 1 - - await hass.services.async_call( - dyson.DYSON_DOMAIN, - dyson.SERVICE_SET_FLOW_DIRECTION_FRONT, - {ATTR_ENTITY_ID: "fan.living_room", dyson.ATTR_FLOW_DIRECTION_FRONT: False}, - True, - ) - assert device.disable_frontal_direction.call_count == 1 - - -@patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_dyson_purecool_device()], -) -async def test_purecool_set_timer(devices, login, hass): - """Test set timer.""" - device = devices.return_value[0] - await async_setup_component(hass, dyson.DYSON_DOMAIN, _get_config()) - await hass.async_block_till_done() - - await hass.services.async_call( - dyson.DYSON_DOMAIN, - dyson.SERVICE_SET_TIMER, - {ATTR_ENTITY_ID: "fan.bed_room", dyson.ATTR_TIMER: 60}, - True, - ) - assert device.enable_frontal_direction.call_count == 0 - - await hass.services.async_call( - dyson.DYSON_DOMAIN, - dyson.SERVICE_SET_TIMER, - {ATTR_ENTITY_ID: "fan.living_room", dyson.ATTR_TIMER: 60}, - True, - ) - device.enable_sleep_timer.assert_called_with(60) - - await hass.services.async_call( - dyson.DYSON_DOMAIN, - dyson.SERVICE_SET_TIMER, - {ATTR_ENTITY_ID: "fan.living_room", dyson.ATTR_TIMER: 0}, - True, - ) - assert device.disable_sleep_timer.call_count == 1 - - -@patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_dyson_purecool_device()], -) -async def test_purecool_update_state(devices, login, hass): - """Test state update.""" - device = devices.return_value[0] - await async_setup_component(hass, dyson.DYSON_DOMAIN, _get_config()) - await hass.async_block_till_done() - event = { - "msg": "CURRENT-STATE", - "product-state": { - "fpwr": "OFF", - "fdir": "OFF", - "auto": "OFF", - "oscs": "ON", - "oson": "ON", - "nmod": "OFF", - "rhtm": "ON", - "fnst": "FAN", - "ercd": "11E1", - "wacd": "NONE", - "nmdv": "0004", - "fnsp": "0002", - "bril": "0002", - "corf": "ON", - "cflr": "0085", - "hflr": "0095", - "sltm": "OFF", - "osal": "0045", - "osau": "0095", - "ancp": "CUST", - }, - } - device.state = DysonPureCoolV2State(json.dumps(event)) - - for call in device.add_message_listener.call_args_list: - callback = call[0][0] - if type(callback.__self__) == dyson.DysonPureCoolDevice: - callback(device.state) - - await hass.async_block_till_done() - fan_state = hass.states.get("fan.living_room") - attributes = fan_state.attributes - - assert fan_state.state == "off" - assert attributes[dyson.ATTR_NIGHT_MODE] is False - assert attributes[dyson.ATTR_AUTO_MODE] is False - assert attributes[dyson.ATTR_ANGLE_LOW] == 45 - assert attributes[dyson.ATTR_ANGLE_HIGH] == 95 - assert attributes[dyson.ATTR_FLOW_DIRECTION_FRONT] is False - assert attributes[dyson.ATTR_TIMER] == "OFF" - assert attributes[dyson.ATTR_HEPA_FILTER] == 95 - assert attributes[dyson.ATTR_CARBON_FILTER] == 85 - assert attributes[dyson.ATTR_DYSON_SPEED] == int(FanSpeed.FAN_SPEED_2.value) - assert attributes[ATTR_SPEED] is SPEED_LOW + device.state.fan_mode = FanMode.AUTO.value + device.state.speed = FanSpeed.FAN_SPEED_AUTO.value + device.state.night_mode = "OFF" + device.state.oscillation = "OFF" + await async_update_device(hass, device, DysonPureCoolState) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + attributes = state.attributes + assert attributes[ATTR_NIGHT_MODE] is False assert attributes[ATTR_OSCILLATING] is False - assert attributes[dyson.ATTR_DYSON_SPEED_LIST] == _get_supported_speeds() + assert attributes[ATTR_SPEED] == SPEED_MEDIUM + assert attributes[ATTR_DYSON_SPEED] == "AUTO" + assert attributes[ATTR_AUTO_MODE] is True -@patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_dyson_purecool_device()], -) -async def test_purecool_update_state_filter_inv(devices, login, hass): - """Test state TP06 carbon filter state.""" - device = devices.return_value[0] - await async_setup_component(hass, dyson.DYSON_DOMAIN, _get_config()) - await hass.async_block_till_done() - event = { - "msg": "CURRENT-STATE", - "product-state": { - "fpwr": "OFF", - "fdir": "ON", - "auto": "ON", - "oscs": "ON", - "oson": "ON", - "nmod": "ON", - "rhtm": "ON", - "fnst": "FAN", - "ercd": "11E1", - "wacd": "NONE", - "nmdv": "0004", - "fnsp": "0002", - "bril": "0002", - "corf": "ON", - "cflr": "INV", - "hflr": "0075", - "sltm": "OFF", - "osal": "0055", - "osau": "0105", - "ancp": "CUST", - }, - } - device.state = DysonPureCoolV2State(json.dumps(event)) +@pytest.mark.parametrize("device", [DysonPureCool], indirect=True) +async def test_state_purecool(hass: HomeAssistant, device: DysonPureCool) -> None: + """Test the state of a PureCool fan.""" + er = await entity_registry.async_get_registry(hass) + assert er.async_get(ENTITY_ID).unique_id == SERIAL - for call in device.add_message_listener.call_args_list: - callback = call[0][0] - if type(callback.__self__) == dyson.DysonPureCoolDevice: - callback(device.state) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ON + assert state.name == NAME + attributes = state.attributes + assert attributes[ATTR_NIGHT_MODE] is True + assert attributes[ATTR_OSCILLATING] is True + assert attributes[ATTR_ANGLE_LOW] == 24 + assert attributes[ATTR_ANGLE_HIGH] == 254 + assert attributes[ATTR_SPEED] == SPEED_LOW + assert attributes[ATTR_SPEED_LIST] == [SPEED_LOW, SPEED_MEDIUM, SPEED_HIGH] + assert attributes[ATTR_DYSON_SPEED] == 1 + assert attributes[ATTR_DYSON_SPEED_LIST] == list(range(1, 11)) + assert attributes[ATTR_AUTO_MODE] is False + assert attributes[ATTR_FLOW_DIRECTION_FRONT] is True + assert attributes[ATTR_TIMER] == "OFF" + assert attributes[ATTR_HEPA_FILTER] == 100 + assert attributes[ATTR_CARBON_FILTER] == 100 + assert attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_OSCILLATE | SUPPORT_SET_SPEED - await hass.async_block_till_done() - fan_state = hass.states.get("fan.living_room") - attributes = fan_state.attributes - - assert fan_state.state == "off" - assert attributes[dyson.ATTR_NIGHT_MODE] is True - assert attributes[dyson.ATTR_AUTO_MODE] is True - assert attributes[dyson.ATTR_ANGLE_LOW] == 55 - assert attributes[dyson.ATTR_ANGLE_HIGH] == 105 - assert attributes[dyson.ATTR_FLOW_DIRECTION_FRONT] is True - assert attributes[dyson.ATTR_TIMER] == "OFF" - assert attributes[dyson.ATTR_HEPA_FILTER] == 75 - assert attributes[dyson.ATTR_CARBON_FILTER] == "INV" - assert attributes[dyson.ATTR_DYSON_SPEED] == int(FanSpeed.FAN_SPEED_2.value) - assert attributes[ATTR_SPEED] is SPEED_LOW + device.state.auto_mode = "ON" + device.state.night_mode = "OFF" + device.state.oscillation = "OIOF" + device.state.speed = "AUTO" + device.state.front_direction = "OFF" + device.state.sleep_timer = "0120" + device.state.carbon_filter_state = "INV" + await async_update_device(hass, device, DysonPureCoolV2State) + state = hass.states.get(ENTITY_ID) + attributes = state.attributes + assert attributes[ATTR_NIGHT_MODE] is False assert attributes[ATTR_OSCILLATING] is False - assert attributes[dyson.ATTR_DYSON_SPEED_LIST] == _get_supported_speeds() + assert attributes[ATTR_SPEED] == SPEED_MEDIUM + assert attributes[ATTR_DYSON_SPEED] == "AUTO" + assert attributes[ATTR_AUTO_MODE] is True + assert attributes[ATTR_FLOW_DIRECTION_FRONT] is False + assert attributes[ATTR_TIMER] == "0120" + assert attributes[ATTR_CARBON_FILTER] == "INV" + + device.state.fan_power = "OFF" + await async_update_device(hass, device, DysonPureCoolV2State) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF -@patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_dyson_purecool_device()], +@pytest.mark.parametrize( + "service,service_data,configuration_args", + [ + (SERVICE_TURN_ON, {}, {"fan_mode": FanMode.FAN}), + ( + SERVICE_TURN_ON, + {ATTR_SPEED: SPEED_LOW}, + {"fan_mode": FanMode.FAN, "fan_speed": FanSpeed.FAN_SPEED_4}, + ), + (SERVICE_TURN_OFF, {}, {"fan_mode": FanMode.OFF}), + ( + SERVICE_OSCILLATE, + {ATTR_OSCILLATING: True}, + {"oscillation": Oscillation.OSCILLATION_ON}, + ), + ( + SERVICE_OSCILLATE, + {ATTR_OSCILLATING: False}, + {"oscillation": Oscillation.OSCILLATION_OFF}, + ), + ( + SERVICE_SET_SPEED, + {ATTR_SPEED: SPEED_LOW}, + {"fan_mode": FanMode.FAN, "fan_speed": FanSpeed.FAN_SPEED_4}, + ), + ( + SERVICE_SET_SPEED, + {ATTR_SPEED: SPEED_MEDIUM}, + {"fan_mode": FanMode.FAN, "fan_speed": FanSpeed.FAN_SPEED_7}, + ), + ( + SERVICE_SET_SPEED, + {ATTR_SPEED: SPEED_HIGH}, + {"fan_mode": FanMode.FAN, "fan_speed": FanSpeed.FAN_SPEED_10}, + ), + ], ) -async def test_purecool_component_setup_only_once(devices, login, hass): - """Test if entities are created only once.""" - config = _get_config() - await async_setup_component(hass, dyson_parent.DOMAIN, config) - await hass.async_block_till_done() - discovery.load_platform(hass, "fan", dyson_parent.DOMAIN, {}, config) - await hass.async_block_till_done() +@pytest.mark.parametrize("device", [DysonPureCoolLink], indirect=True) +async def test_commands_purecoollink( + hass: HomeAssistant, + device: DysonPureCoolLink, + service: str, + service_data: dict, + configuration_args: dict, +) -> None: + """Test sending commands to a PureCoolLink fan.""" + await hass.services.async_call( + PLATFORM_DOMAIN, + service, + { + ATTR_ENTITY_ID: ENTITY_ID, + **service_data, + }, + blocking=True, + ) + device.set_configuration.assert_called_once_with(**configuration_args) - fans = [ - fan - for fan in hass.data[DOMAIN].entities - if fan.platform.platform_name == dyson_parent.DOMAIN - ] - assert len(fans) == 1 - assert fans[0].device_serial == "XX-XXXXX-XX" +@pytest.mark.parametrize( + "service,service_data,command,command_args", + [ + (SERVICE_TURN_ON, {}, "turn_on", []), + ( + SERVICE_TURN_ON, + {ATTR_SPEED: SPEED_LOW}, + "set_fan_speed", + [FanSpeed.FAN_SPEED_4], + ), + (SERVICE_TURN_OFF, {}, "turn_off", []), + (SERVICE_OSCILLATE, {ATTR_OSCILLATING: True}, "enable_oscillation", []), + (SERVICE_OSCILLATE, {ATTR_OSCILLATING: False}, "disable_oscillation", []), + ( + SERVICE_SET_SPEED, + {ATTR_SPEED: SPEED_LOW}, + "set_fan_speed", + [FanSpeed.FAN_SPEED_4], + ), + ( + SERVICE_SET_SPEED, + {ATTR_SPEED: SPEED_MEDIUM}, + "set_fan_speed", + [FanSpeed.FAN_SPEED_7], + ), + ( + SERVICE_SET_SPEED, + {ATTR_SPEED: SPEED_HIGH}, + "set_fan_speed", + [FanSpeed.FAN_SPEED_10], + ), + ], +) +@pytest.mark.parametrize("device", [DysonPureCool], indirect=True) +async def test_commands_purecool( + hass: HomeAssistant, + device: DysonPureCool, + service: str, + service_data: dict, + command: str, + command_args: list, +) -> None: + """Test sending commands to a PureCool fan.""" + await hass.services.async_call( + PLATFORM_DOMAIN, + service, + { + ATTR_ENTITY_ID: ENTITY_ID, + **service_data, + }, + blocking=True, + ) + getattr(device, command).assert_called_once_with(*command_args) + + +@pytest.mark.parametrize( + "service,service_data,configuration_args", + [ + ( + SERVICE_SET_NIGHT_MODE, + {ATTR_NIGHT_MODE: True}, + {"night_mode": NightMode.NIGHT_MODE_ON}, + ), + ( + SERVICE_SET_NIGHT_MODE, + {ATTR_NIGHT_MODE: False}, + {"night_mode": NightMode.NIGHT_MODE_OFF}, + ), + (SERVICE_SET_AUTO_MODE, {"auto_mode": True}, {"fan_mode": FanMode.AUTO}), + (SERVICE_SET_AUTO_MODE, {"auto_mode": False}, {"fan_mode": FanMode.FAN}), + ( + SERVICE_SET_DYSON_SPEED, + {ATTR_DYSON_SPEED: "4"}, + {"fan_mode": FanMode.FAN, "fan_speed": FanSpeed.FAN_SPEED_4}, + ), + ], +) +@pytest.mark.parametrize("device", [DysonPureCoolLink], indirect=True) +async def test_custom_services_purecoollink( + hass: HomeAssistant, + device: DysonPureCoolLink, + service: str, + service_data: dict, + configuration_args: dict, +) -> None: + """Test custom services of a PureCoolLink fan.""" + await hass.services.async_call( + DOMAIN, + service, + { + ATTR_ENTITY_ID: ENTITY_ID, + **service_data, + }, + blocking=True, + ) + device.set_configuration.assert_called_once_with(**configuration_args) + + +@pytest.mark.parametrize( + "service,service_data,command,command_args", + [ + (SERVICE_SET_NIGHT_MODE, {ATTR_NIGHT_MODE: True}, "enable_night_mode", []), + (SERVICE_SET_NIGHT_MODE, {ATTR_NIGHT_MODE: False}, "disable_night_mode", []), + (SERVICE_SET_AUTO_MODE, {ATTR_AUTO_MODE: True}, "enable_auto_mode", []), + (SERVICE_SET_AUTO_MODE, {ATTR_AUTO_MODE: False}, "disable_auto_mode", []), + (SERVICE_SET_AUTO_MODE, {ATTR_AUTO_MODE: False}, "disable_auto_mode", []), + ( + SERVICE_SET_ANGLE, + {ATTR_ANGLE_LOW: 10, ATTR_ANGLE_HIGH: 200}, + "enable_oscillation", + [10, 200], + ), + ( + SERVICE_SET_FLOW_DIRECTION_FRONT, + {ATTR_FLOW_DIRECTION_FRONT: True}, + "enable_frontal_direction", + [], + ), + ( + SERVICE_SET_FLOW_DIRECTION_FRONT, + {ATTR_FLOW_DIRECTION_FRONT: False}, + "disable_frontal_direction", + [], + ), + (SERVICE_SET_TIMER, {ATTR_TIMER: 0}, "disable_sleep_timer", []), + (SERVICE_SET_TIMER, {ATTR_TIMER: 10}, "enable_sleep_timer", [10]), + ( + SERVICE_SET_DYSON_SPEED, + {ATTR_DYSON_SPEED: "4"}, + "set_fan_speed", + [FanSpeed("0004")], + ), + ], +) +@pytest.mark.parametrize("device", [DysonPureCool], indirect=True) +async def test_custom_services_purecool( + hass: HomeAssistant, + device: DysonPureCool, + service: str, + service_data: dict, + command: str, + command_args: list, +) -> None: + """Test custom services of a PureCool fan.""" + await hass.services.async_call( + DOMAIN, + service, + { + ATTR_ENTITY_ID: ENTITY_ID, + **service_data, + }, + blocking=True, + ) + getattr(device, command).assert_called_once_with(*command_args) + + +@pytest.mark.parametrize( + "domain,service,data", + [ + (PLATFORM_DOMAIN, SERVICE_TURN_ON, {ATTR_SPEED: "AUTO"}), + (PLATFORM_DOMAIN, SERVICE_SET_SPEED, {ATTR_SPEED: "AUTO"}), + (DOMAIN, SERVICE_SET_DYSON_SPEED, {ATTR_DYSON_SPEED: "11"}), + ], +) +@pytest.mark.parametrize("device", [DysonPureCool], indirect=True) +async def test_custom_services_invalid_data( + hass: HomeAssistant, device: DysonPureCool, domain: str, service: str, data: dict +) -> None: + """Test custom services calling with invalid data.""" + with pytest.raises(ValueError): + await hass.services.async_call( + domain, + service, + { + ATTR_ENTITY_ID: ENTITY_ID, + **data, + }, + blocking=True, + ) diff --git a/tests/components/dyson/test_init.py b/tests/components/dyson/test_init.py index d2c36beb7d5..2535da4d166 100644 --- a/tests/components/dyson/test_init.py +++ b/tests/components/dyson/test_init.py @@ -1,231 +1,100 @@ """Test the parent Dyson component.""" -import unittest -from unittest import mock +import copy +from unittest.mock import MagicMock, patch -from homeassistant.components import dyson +from homeassistant.components.dyson import DOMAIN +from homeassistant.const import CONF_DEVICES +from homeassistant.core import HomeAssistant -from .common import load_mock_device +from .common import ( + BASE_PATH, + CONFIG, + ENTITY_NAME, + IP_ADDRESS, + async_get_360eye_device, + async_get_purecool_device, + async_get_purecoollink_device, +) -from tests.common import get_test_home_assistant +from tests.common import async_setup_component -def _get_dyson_account_device_available(): - """Return a valid device provide by Dyson web services.""" - device = mock.Mock() - load_mock_device(device) - device.connect = mock.Mock(return_value=True) - device.auto_connect = mock.Mock(return_value=True) - return device +async def test_setup_manual(hass: HomeAssistant): + """Test set up the component with manually configured device IPs.""" + SERIAL_TEMPLATE = "XX-XXXXX-X{}" + + # device1 works + device1 = async_get_purecoollink_device() + device1.serial = SERIAL_TEMPLATE.format(1) + + # device2 failed to connect + device2 = async_get_purecool_device() + device2.serial = SERIAL_TEMPLATE.format(2) + device2.connect = MagicMock(return_value=False) + + # device3 throws exception during connection + device3 = async_get_360eye_device() + device3.serial = SERIAL_TEMPLATE.format(3) + device3.connect = MagicMock(side_effect=OSError) + + # device4 not configured in configuration + device4 = async_get_360eye_device() + device4.serial = SERIAL_TEMPLATE.format(4) + + devices = [device1, device2, device3, device4] + config = copy.deepcopy(CONFIG) + config[DOMAIN][CONF_DEVICES] = [ + { + "device_id": SERIAL_TEMPLATE.format(i), + "device_ip": IP_ADDRESS, + } + for i in [1, 2, 3, 5] # 1 device missing and 1 device not existed + ] + + with patch(f"{BASE_PATH}.DysonAccount.login", return_value=True) as login, patch( + f"{BASE_PATH}.DysonAccount.devices", return_value=devices + ) as devices_method, patch( + f"{BASE_PATH}.DYSON_PLATFORMS", ["fan", "vacuum"] + ): # Patch platforms to get rid of sensors + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + login.assert_called_once_with() + devices_method.assert_called_once_with() + + # Only one fan and zero vacuum is set up successfully + assert hass.states.async_entity_ids() == [f"fan.{ENTITY_NAME}"] + device1.connect.assert_called_once_with(IP_ADDRESS) + device2.connect.assert_called_once_with(IP_ADDRESS) + device3.connect.assert_called_once_with(IP_ADDRESS) + device4.connect.assert_not_called() -def _get_dyson_account_device_not_available(): - """Return an invalid device provide by Dyson web services.""" - device = mock.Mock() - load_mock_device(device) - device.connect = mock.Mock(return_value=False) - device.auto_connect = mock.Mock(return_value=False) - return device +async def test_setup_autoconnect(hass: HomeAssistant): + """Test set up the component with auto connect.""" + # device1 works + device1 = async_get_purecoollink_device() + + # device2 failed to auto connect + device2 = async_get_purecool_device() + device2.auto_connect = MagicMock(return_value=False) + + devices = [device1, device2] + config = copy.deepcopy(CONFIG) + config[DOMAIN].pop(CONF_DEVICES) + + with patch(f"{BASE_PATH}.DysonAccount.login", return_value=True), patch( + f"{BASE_PATH}.DysonAccount.devices", return_value=devices + ), patch( + f"{BASE_PATH}.DYSON_PLATFORMS", ["fan"] + ): # Patch platforms to get rid of sensors + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + assert hass.states.async_entity_ids_count() == 1 -def _get_dyson_account_device_error(): - """Return an invalid device raising OSError while connecting.""" - device = mock.Mock() - load_mock_device(device) - device.connect = mock.Mock(side_effect=OSError("Network error")) - return device - - -class DysonTest(unittest.TestCase): - """Dyson parent component test class.""" - - def setUp(self): # pylint: disable=invalid-name - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.addCleanup(self.tear_down_cleanup) - - def tear_down_cleanup(self): - """Stop everything that was started.""" - self.hass.stop() - - @mock.patch("libpurecool.dyson.DysonAccount.login", return_value=False) - def test_dyson_login_failed(self, mocked_login): - """Test if Dyson connection failed.""" - dyson.setup( - self.hass, - { - dyson.DOMAIN: { - dyson.CONF_USERNAME: "email", - dyson.CONF_PASSWORD: "password", - dyson.CONF_LANGUAGE: "FR", - } - }, - ) - assert mocked_login.call_count == 1 - - @mock.patch("libpurecool.dyson.DysonAccount.devices", return_value=[]) - @mock.patch("libpurecool.dyson.DysonAccount.login", return_value=True) - def test_dyson_login(self, mocked_login, mocked_devices): - """Test valid connection to dyson web service.""" - dyson.setup( - self.hass, - { - dyson.DOMAIN: { - dyson.CONF_USERNAME: "email", - dyson.CONF_PASSWORD: "password", - dyson.CONF_LANGUAGE: "FR", - } - }, - ) - assert mocked_login.call_count == 1 - assert mocked_devices.call_count == 1 - assert len(self.hass.data[dyson.DYSON_DEVICES]) == 0 - - @mock.patch("homeassistant.helpers.discovery.load_platform") - @mock.patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_dyson_account_device_available()], - ) - @mock.patch("libpurecool.dyson.DysonAccount.login", return_value=True) - def test_dyson_custom_conf(self, mocked_login, mocked_devices, mocked_discovery): - """Test device connection using custom configuration.""" - dyson.setup( - self.hass, - { - dyson.DOMAIN: { - dyson.CONF_USERNAME: "email", - dyson.CONF_PASSWORD: "password", - dyson.CONF_LANGUAGE: "FR", - dyson.CONF_DEVICES: [ - {"device_id": "XX-XXXXX-XX", "device_ip": "192.168.0.1"} - ], - } - }, - ) - assert mocked_login.call_count == 1 - assert mocked_devices.call_count == 1 - assert len(self.hass.data[dyson.DYSON_DEVICES]) == 1 - assert mocked_discovery.call_count == 5 - - @mock.patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_dyson_account_device_not_available()], - ) - @mock.patch("libpurecool.dyson.DysonAccount.login", return_value=True) - def test_dyson_custom_conf_device_not_available(self, mocked_login, mocked_devices): - """Test device connection with an invalid device.""" - dyson.setup( - self.hass, - { - dyson.DOMAIN: { - dyson.CONF_USERNAME: "email", - dyson.CONF_PASSWORD: "password", - dyson.CONF_LANGUAGE: "FR", - dyson.CONF_DEVICES: [ - {"device_id": "XX-XXXXX-XX", "device_ip": "192.168.0.1"} - ], - } - }, - ) - assert mocked_login.call_count == 1 - assert mocked_devices.call_count == 1 - assert len(self.hass.data[dyson.DYSON_DEVICES]) == 0 - - @mock.patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_dyson_account_device_error()], - ) - @mock.patch("libpurecool.dyson.DysonAccount.login", return_value=True) - def test_dyson_custom_conf_device_error(self, mocked_login, mocked_devices): - """Test device connection with device raising an exception.""" - dyson.setup( - self.hass, - { - dyson.DOMAIN: { - dyson.CONF_USERNAME: "email", - dyson.CONF_PASSWORD: "password", - dyson.CONF_LANGUAGE: "FR", - dyson.CONF_DEVICES: [ - {"device_id": "XX-XXXXX-XX", "device_ip": "192.168.0.1"} - ], - } - }, - ) - assert mocked_login.call_count == 1 - assert mocked_devices.call_count == 1 - assert len(self.hass.data[dyson.DYSON_DEVICES]) == 0 - - @mock.patch("homeassistant.helpers.discovery.load_platform") - @mock.patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_dyson_account_device_available()], - ) - @mock.patch("libpurecool.dyson.DysonAccount.login", return_value=True) - def test_dyson_custom_conf_with_unknown_device( - self, mocked_login, mocked_devices, mocked_discovery - ): - """Test device connection with custom conf and unknown device.""" - dyson.setup( - self.hass, - { - dyson.DOMAIN: { - dyson.CONF_USERNAME: "email", - dyson.CONF_PASSWORD: "password", - dyson.CONF_LANGUAGE: "FR", - dyson.CONF_DEVICES: [ - {"device_id": "XX-XXXXX-XY", "device_ip": "192.168.0.1"} - ], - } - }, - ) - assert mocked_login.call_count == 1 - assert mocked_devices.call_count == 1 - assert len(self.hass.data[dyson.DYSON_DEVICES]) == 0 - assert mocked_discovery.call_count == 0 - - @mock.patch("homeassistant.helpers.discovery.load_platform") - @mock.patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_dyson_account_device_available()], - ) - @mock.patch("libpurecool.dyson.DysonAccount.login", return_value=True) - def test_dyson_discovery(self, mocked_login, mocked_devices, mocked_discovery): - """Test device connection using discovery.""" - dyson.setup( - self.hass, - { - dyson.DOMAIN: { - dyson.CONF_USERNAME: "email", - dyson.CONF_PASSWORD: "password", - dyson.CONF_LANGUAGE: "FR", - dyson.CONF_TIMEOUT: 5, - dyson.CONF_RETRY: 2, - } - }, - ) - assert mocked_login.call_count == 1 - assert mocked_devices.call_count == 1 - assert len(self.hass.data[dyson.DYSON_DEVICES]) == 1 - assert mocked_discovery.call_count == 5 - - @mock.patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_dyson_account_device_not_available()], - ) - @mock.patch("libpurecool.dyson.DysonAccount.login", return_value=True) - def test_dyson_discovery_device_not_available(self, mocked_login, mocked_devices): - """Test device connection with discovery and invalid device.""" - dyson.setup( - self.hass, - { - dyson.DOMAIN: { - dyson.CONF_USERNAME: "email", - dyson.CONF_PASSWORD: "password", - dyson.CONF_LANGUAGE: "FR", - dyson.CONF_TIMEOUT: 5, - dyson.CONF_RETRY: 2, - } - }, - ) - assert mocked_login.call_count == 1 - assert mocked_devices.call_count == 1 - assert len(self.hass.data[dyson.DYSON_DEVICES]) == 0 +async def test_login_failed(hass: HomeAssistant): + """Test login failure during setup.""" + with patch(f"{BASE_PATH}.DysonAccount.login", return_value=False): + assert not await async_setup_component(hass, DOMAIN, CONFIG) + await hass.async_block_till_done() diff --git a/tests/components/dyson/test_sensor.py b/tests/components/dyson/test_sensor.py index 4acc52743bb..a1f8e4bb37c 100644 --- a/tests/components/dyson/test_sensor.py +++ b/tests/components/dyson/test_sensor.py @@ -1,351 +1,182 @@ """Test the Dyson sensor(s) component.""" -import unittest -from unittest import mock +from typing import List, Type +from unittest.mock import patch from libpurecool.dyson_pure_cool import DysonPureCool from libpurecool.dyson_pure_cool_link import DysonPureCoolLink +import pytest -from homeassistant.components import dyson as dyson_parent -from homeassistant.components.dyson import sensor as dyson +from homeassistant.components.dyson import DOMAIN +from homeassistant.components.dyson.sensor import SENSOR_ATTRIBUTES, SENSOR_NAMES +from homeassistant.components.sensor import DOMAIN as PLATFORM_DOMAIN from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, - PERCENTAGE, STATE_OFF, TEMP_CELSIUS, TEMP_FAHRENHEIT, - TIME_HOURS, ) -from homeassistant.helpers import discovery -from homeassistant.setup import async_setup_component +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry +from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM, UnitSystem -from .common import load_mock_device +from .common import ( + BASE_PATH, + CONFIG, + ENTITY_NAME, + NAME, + SERIAL, + async_get_basic_device, + async_update_device, +) -from tests.async_mock import patch -from tests.common import get_test_home_assistant +from tests.common import async_setup_component + +ENTITY_ID_PREFIX = f"{PLATFORM_DOMAIN}.{ENTITY_NAME}" + +MOCKED_VALUES = { + "filter_life": 100, + "dust": 5, + "humidity": 45, + "temperature_kelvin": 295, + "temperature": 21.9, + "air_quality": 5, + "hepa_filter_state": 50, + "combi_filter_state": 50, + "carbon_filter_state": 10, +} + +MOCKED_UPDATED_VALUES = { + "filter_life": 30, + "dust": 2, + "humidity": 80, + "temperature_kelvin": 240, + "temperature": -33.1, + "air_quality": 3, + "hepa_filter_state": 30, + "combi_filter_state": 30, + "carbon_filter_state": 20, +} -def _get_dyson_purecool_device(): - """Return a valid device provide by Dyson web services.""" - device = mock.Mock(spec=DysonPureCool) - load_mock_device(device) - return device - - -def _get_config(): - """Return a config dictionary.""" - return { - dyson_parent.DOMAIN: { - dyson_parent.CONF_USERNAME: "email", - dyson_parent.CONF_PASSWORD: "password", - dyson_parent.CONF_LANGUAGE: "GB", - dyson_parent.CONF_DEVICES: [ - {"device_id": "XX-XXXXX-XX", "device_ip": "192.168.0.1"} - ], - } - } - - -def _get_device_without_state(): - """Return a valid device provide by Dyson web services.""" - device = mock.Mock(spec=DysonPureCoolLink) - device.name = "Device_name" - device.state = None - device.environmental_state = None - return device - - -def _get_with_state(): - """Return a valid device with state values.""" - device = mock.Mock() - load_mock_device(device) - device.name = "Device_name" - device.state.filter_life = 100 - device.environmental_state.dust = 5 - device.environmental_state.humidity = 45 - device.environmental_state.temperature = 295 - device.environmental_state.volatil_organic_compounds = 2 - - return device - - -def _get_purecool_device(): - """Return a valid device with filters life state values.""" - device = mock.Mock(spec=DysonPureCool) - load_mock_device(device) - device.name = "PureCool" - device.state.carbon_filter_state = "0096" - device.state.hepa_filter_state = "0056" - device.environmental_state.dust = 5 - device.environmental_state.humidity = 45 - device.environmental_state.temperature = 295 - device.environmental_state.volatil_organic_compounds = 2 - - return device - - -def _get_purecool_humidify_device(): - """Return a valid device with filters life state values.""" - device = mock.Mock(spec=DysonPureCool) - load_mock_device(device) - device.name = "PureCool_Humidify" - device.state.carbon_filter_state = "INV" - device.state.hepa_filter_state = "0075" - device.environmental_state.dust = 5 - device.environmental_state.humidity = 45 - device.environmental_state.temperature = 295 - device.environmental_state.volatil_organic_compounds = 2 - - return device - - -def _get_with_standby_monitoring(): - """Return a valid device with state but with standby monitoring disable.""" - device = mock.Mock() - load_mock_device(device) - device.name = "Device_name" - device.environmental_state.humidity = 0 - device.environmental_state.temperature = 0 - - return device - - -class DysonTest(unittest.TestCase): - """Dyson Sensor component test class.""" - - def setUp(self): # pylint: disable=invalid-name - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.addCleanup(self.tear_down_cleanup) - - def tear_down_cleanup(self): - """Stop everything that was started.""" - self.hass.stop() - - def test_setup_component_with_no_devices(self): - """Test setup component with no devices.""" - self.hass.data[dyson.DYSON_DEVICES] = [] - add_entities = mock.MagicMock() - dyson.setup_platform(self.hass, None, add_entities) - add_entities.assert_not_called() - - def test_setup_component(self): - """Test setup component with devices.""" - - def _add_device(devices): - assert len(devices) == 5 - assert devices[0].name == "Device_name Filter Life" - assert devices[1].name == "Device_name Dust" - assert devices[2].name == "Device_name Humidity" - assert devices[3].name == "Device_name Temperature" - assert devices[4].name == "Device_name AQI" - - device_fan = _get_device_without_state() - device_non_fan = _get_with_state() - self.hass.data[dyson.DYSON_DEVICES] = [ - device_fan, - device_non_fan, - ] - dyson.setup_platform(self.hass, None, _add_device, mock.MagicMock()) - - def test_dyson_filter_life_sensor(self): - """Test filter life sensor with no value.""" - sensor = dyson.DysonFilterLifeSensor(_get_device_without_state()) - sensor.hass = self.hass - sensor.entity_id = "sensor.dyson_1" - assert not sensor.should_poll - assert sensor.state is None - assert sensor.unit_of_measurement == TIME_HOURS - assert sensor.name == "Device_name Filter Life" - assert sensor.entity_id == "sensor.dyson_1" - sensor.on_message("message") - - def test_dyson_filter_life_sensor_with_values(self): - """Test filter sensor with values.""" - sensor = dyson.DysonFilterLifeSensor(_get_with_state()) - sensor.hass = self.hass - sensor.entity_id = "sensor.dyson_1" - assert not sensor.should_poll - assert sensor.state == 100 - assert sensor.unit_of_measurement == TIME_HOURS - assert sensor.name == "Device_name Filter Life" - assert sensor.entity_id == "sensor.dyson_1" - sensor.on_message("message") - - def test_dyson_dust_sensor(self): - """Test dust sensor with no value.""" - sensor = dyson.DysonDustSensor(_get_device_without_state()) - sensor.hass = self.hass - sensor.entity_id = "sensor.dyson_1" - assert not sensor.should_poll - assert sensor.state is None - assert sensor.unit_of_measurement is None - assert sensor.name == "Device_name Dust" - assert sensor.entity_id == "sensor.dyson_1" - - def test_dyson_dust_sensor_with_values(self): - """Test dust sensor with values.""" - sensor = dyson.DysonDustSensor(_get_with_state()) - sensor.hass = self.hass - sensor.entity_id = "sensor.dyson_1" - assert not sensor.should_poll - assert sensor.state == 5 - assert sensor.unit_of_measurement is None - assert sensor.name == "Device_name Dust" - assert sensor.entity_id == "sensor.dyson_1" - - def test_dyson_humidity_sensor(self): - """Test humidity sensor with no value.""" - sensor = dyson.DysonHumiditySensor(_get_device_without_state()) - sensor.hass = self.hass - sensor.entity_id = "sensor.dyson_1" - assert not sensor.should_poll - assert sensor.state is None - assert sensor.unit_of_measurement == PERCENTAGE - assert sensor.name == "Device_name Humidity" - assert sensor.entity_id == "sensor.dyson_1" - - def test_dyson_humidity_sensor_with_values(self): - """Test humidity sensor with values.""" - sensor = dyson.DysonHumiditySensor(_get_with_state()) - sensor.hass = self.hass - sensor.entity_id = "sensor.dyson_1" - assert not sensor.should_poll - assert sensor.state == 45 - assert sensor.unit_of_measurement == PERCENTAGE - assert sensor.name == "Device_name Humidity" - assert sensor.entity_id == "sensor.dyson_1" - - def test_dyson_humidity_standby_monitoring(self): - """Test humidity sensor while device is in standby monitoring.""" - sensor = dyson.DysonHumiditySensor(_get_with_standby_monitoring()) - sensor.hass = self.hass - sensor.entity_id = "sensor.dyson_1" - assert not sensor.should_poll - assert sensor.state == STATE_OFF - assert sensor.unit_of_measurement == PERCENTAGE - assert sensor.name == "Device_name Humidity" - assert sensor.entity_id == "sensor.dyson_1" - - def test_dyson_temperature_sensor(self): - """Test temperature sensor with no value.""" - sensor = dyson.DysonTemperatureSensor(_get_device_without_state(), TEMP_CELSIUS) - sensor.hass = self.hass - sensor.entity_id = "sensor.dyson_1" - assert not sensor.should_poll - assert sensor.state is None - assert sensor.unit_of_measurement == TEMP_CELSIUS - assert sensor.name == "Device_name Temperature" - assert sensor.entity_id == "sensor.dyson_1" - - def test_dyson_temperature_sensor_with_values(self): - """Test temperature sensor with values.""" - sensor = dyson.DysonTemperatureSensor(_get_with_state(), TEMP_CELSIUS) - sensor.hass = self.hass - sensor.entity_id = "sensor.dyson_1" - assert not sensor.should_poll - assert sensor.state == 21.9 - assert sensor.unit_of_measurement == TEMP_CELSIUS - assert sensor.name == "Device_name Temperature" - assert sensor.entity_id == "sensor.dyson_1" - - sensor = dyson.DysonTemperatureSensor(_get_with_state(), TEMP_FAHRENHEIT) - sensor.hass = self.hass - sensor.entity_id = "sensor.dyson_1" - assert not sensor.should_poll - assert sensor.state == 71.3 - assert sensor.unit_of_measurement == TEMP_FAHRENHEIT - assert sensor.name == "Device_name Temperature" - assert sensor.entity_id == "sensor.dyson_1" - - def test_dyson_temperature_standby_monitoring(self): - """Test temperature sensor while device is in standby monitoring.""" - sensor = dyson.DysonTemperatureSensor( - _get_with_standby_monitoring(), TEMP_CELSIUS +@callback +def _async_assign_values( + device: DysonPureCoolLink, values=MOCKED_VALUES, combi=False +) -> None: + """Assign mocked values to the device.""" + if isinstance(device, DysonPureCool): + device.state.hepa_filter_state = values["hepa_filter_state"] + device.state.carbon_filter_state = ( + "INV" if combi else values["carbon_filter_state"] ) - sensor.hass = self.hass - sensor.entity_id = "sensor.dyson_1" - assert not sensor.should_poll - assert sensor.state == STATE_OFF - assert sensor.unit_of_measurement == TEMP_CELSIUS - assert sensor.name == "Device_name Temperature" - assert sensor.entity_id == "sensor.dyson_1" - - def test_dyson_air_quality_sensor(self): - """Test air quality sensor with no value.""" - sensor = dyson.DysonAirQualitySensor(_get_device_without_state()) - sensor.hass = self.hass - sensor.entity_id = "sensor.dyson_1" - assert not sensor.should_poll - assert sensor.state is None - assert sensor.unit_of_measurement is None - assert sensor.name == "Device_name AQI" - assert sensor.entity_id == "sensor.dyson_1" - - def test_dyson_air_quality_sensor_with_values(self): - """Test air quality sensor with values.""" - sensor = dyson.DysonAirQualitySensor(_get_with_state()) - sensor.hass = self.hass - sensor.entity_id = "sensor.dyson_1" - assert not sensor.should_poll - assert sensor.state == 2 - assert sensor.unit_of_measurement is None - assert sensor.name == "Device_name AQI" - assert sensor.entity_id == "sensor.dyson_1" + device.environmental_state.humidity = values["humidity"] + device.environmental_state.temperature = values["temperature_kelvin"] + else: # DysonPureCoolLink + device.state.filter_life = values["filter_life"] + device.environmental_state.dust = values["dust"] + device.environmental_state.humidity = values["humidity"] + device.environmental_state.temperature = values["temperature_kelvin"] + device.environmental_state.volatil_organic_compounds = values["air_quality"] -@patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_dyson_purecool_device()], +@callback +def async_get_device(spec: Type[DysonPureCoolLink], combi=False) -> DysonPureCoolLink: + """Return a device of the given type.""" + device = async_get_basic_device(spec) + _async_assign_values(device, combi=combi) + return device + + +@callback +def _async_get_entity_id(sensor_type: str) -> str: + """Get the expected entity id from the type of the sensor.""" + sensor_name = SENSOR_NAMES[sensor_type] + entity_id_suffix = sensor_name.lower().replace(" ", "_") + return f"{ENTITY_ID_PREFIX}_{entity_id_suffix}" + + +@pytest.mark.parametrize( + "device,sensors", + [ + ( + DysonPureCoolLink, + ["filter_life", "dust", "humidity", "temperature", "air_quality"], + ), + ( + DysonPureCool, + ["hepa_filter_state", "carbon_filter_state", "humidity", "temperature"], + ), + ( + [DysonPureCool, True], + ["combi_filter_state", "humidity", "temperature"], + ), + ], + indirect=["device"], ) -async def test_purecool_component_setup_only_once(devices, login, hass): - """Test if entities are created only once.""" - config = _get_config() - await async_setup_component(hass, dyson_parent.DOMAIN, config) - await hass.async_block_till_done() - discovery.load_platform(hass, "sensor", dyson_parent.DOMAIN, {}, config) - await hass.async_block_till_done() +async def test_sensors( + hass: HomeAssistant, device: DysonPureCoolLink, sensors: List[str] +) -> None: + """Test the sensors.""" + # Temperature is given by the device in kelvin + # Make sure no other sensors are set up + assert len(hass.states.async_all()) == len(sensors) - assert len(hass.data[dyson.DYSON_SENSOR_DEVICES]) == 4 + er = await entity_registry.async_get_registry(hass) + for sensor in sensors: + entity_id = _async_get_entity_id(sensor) + + # Test unique id + assert er.async_get(entity_id).unique_id == f"{SERIAL}-{sensor}" + + # Test state + state = hass.states.get(entity_id) + assert state.state == str(MOCKED_VALUES[sensor]) + assert state.name == f"{NAME} {SENSOR_NAMES[sensor]}" + + # Test attributes + attributes = state.attributes + for attr, value in SENSOR_ATTRIBUTES[sensor].items(): + assert attributes[attr] == value + + # Test data update + _async_assign_values(device, MOCKED_UPDATED_VALUES) + await async_update_device(hass, device) + for sensor in sensors: + state = hass.states.get(_async_get_entity_id(sensor)) + assert state.state == str(MOCKED_UPDATED_VALUES[sensor]) -@patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_purecool_device()], +@pytest.mark.parametrize("device", [DysonPureCoolLink], indirect=True) +async def test_sensors_off(hass: HomeAssistant, device: DysonPureCoolLink) -> None: + """Test the case where temperature and humidity are not available.""" + device.environmental_state.temperature = 0 + device.environmental_state.humidity = 0 + await async_update_device(hass, device) + assert hass.states.get(f"{ENTITY_ID_PREFIX}_temperature").state == STATE_OFF + assert hass.states.get(f"{ENTITY_ID_PREFIX}_humidity").state == STATE_OFF + + +@pytest.mark.parametrize( + "unit_system,temp_unit,temperature", + [(METRIC_SYSTEM, TEMP_CELSIUS, 21.9), (IMPERIAL_SYSTEM, TEMP_FAHRENHEIT, 71.3)], ) -async def test_dyson_purecool_filter_state_sensor(devices, login, hass): - """Test filter sensor with values.""" - config = _get_config() - await async_setup_component(hass, dyson_parent.DOMAIN, config) - await hass.async_block_till_done() +async def test_temperature( + hass: HomeAssistant, unit_system: UnitSystem, temp_unit: str, temperature: float +) -> None: + """Test the temperature sensor in different units.""" + hass.config.units = unit_system - state = hass.states.get("sensor.purecool_hepa_filter_remaining_life") - assert state is not None - assert state.state == "56" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE - assert state.name == "PureCool HEPA Filter Remaining Life" + device = async_get_device(DysonPureCoolLink) + with patch(f"{BASE_PATH}.DysonAccount.login", return_value=True), patch( + f"{BASE_PATH}.DysonAccount.devices", return_value=[device] + ), patch(f"{BASE_PATH}.DYSON_PLATFORMS", [PLATFORM_DOMAIN]): + # DYSON_PLATFORMS is patched so that only the platform being tested is set up + await async_setup_component( + hass, + DOMAIN, + CONFIG, + ) + await hass.async_block_till_done() - state = hass.states.get("sensor.purecool_carbon_filter_remaining_life") - assert state is not None - assert state.state == "96" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE - assert state.name == "PureCool Carbon Filter Remaining Life" - - -@patch("libpurecool.dyson.DysonAccount.login", return_value=True) -@patch( - "libpurecool.dyson.DysonAccount.devices", - return_value=[_get_purecool_humidify_device()], -) -async def test_dyson_purecool_humidify_filter_state_sensor(devices, login, hass): - """Test filter sensor with values.""" - config = _get_config() - await async_setup_component(hass, dyson_parent.DOMAIN, config) - await hass.async_block_till_done() - - state = hass.states.get("sensor.purecool_humidify_combi_filter_remaining_life") - assert state is not None - assert state.state == "75" - assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE - assert state.name == "PureCool_Humidify Combi Filter Remaining Life" + state = hass.states.get(f"{ENTITY_ID_PREFIX}_temperature") + assert state.state == str(temperature) + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == temp_unit diff --git a/tests/components/dyson/test_vacuum.py b/tests/components/dyson/test_vacuum.py index 0ff19665807..03a3b076b02 100644 --- a/tests/components/dyson/test_vacuum.py +++ b/tests/components/dyson/test_vacuum.py @@ -1,189 +1,115 @@ """Test the Dyson 360 eye robot vacuum component.""" -import unittest -from unittest import mock - from libpurecool.const import Dyson360EyeMode, PowerMode from libpurecool.dyson_360_eye import Dyson360Eye +import pytest -from homeassistant.components.dyson import vacuum as dyson -from homeassistant.components.dyson.vacuum import Dyson360EyeDevice +from homeassistant.components.dyson.vacuum import ATTR_POSITION, SUPPORT_DYSON +from homeassistant.components.vacuum import ( + ATTR_FAN_SPEED, + ATTR_FAN_SPEED_LIST, + ATTR_STATUS, + DOMAIN as PLATFORM_DOMAIN, + SERVICE_RETURN_TO_BASE, + SERVICE_SET_FAN_SPEED, + SERVICE_START_PAUSE, + SERVICE_STOP, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.const import ( + ATTR_BATTERY_LEVEL, + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + STATE_OFF, + STATE_ON, +) +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers import entity_registry -from tests.common import get_test_home_assistant +from .common import ( + ENTITY_NAME, + NAME, + SERIAL, + async_get_360eye_device, + async_update_device, +) + +ENTITY_ID = f"{PLATFORM_DOMAIN}.{ENTITY_NAME}" -def _get_non_vacuum_device(): - """Return a non vacuum device.""" - device = mock.Mock() - device.name = "Device_Fan" - device.state = None - return device +@callback +def async_get_device(state=Dyson360EyeMode.FULL_CLEAN_RUNNING) -> Dyson360Eye: + """Return a Dyson 360 Eye device.""" + return async_get_360eye_device(state) -def _get_vacuum_device_cleaning(): - """Return a vacuum device running.""" - device = mock.Mock(spec=Dyson360Eye) - device.name = "Device_Vacuum" - device.state = mock.MagicMock() - device.state.state = Dyson360EyeMode.FULL_CLEAN_RUNNING - device.state.battery_level = 85 - device.state.power_mode = PowerMode.QUIET - device.state.position = (0, 0) - return device +async def test_state(hass: HomeAssistant, device: Dyson360Eye) -> None: + """Test the state of the vacuum.""" + er = await entity_registry.async_get_registry(hass) + assert er.async_get(ENTITY_ID).unique_id == SERIAL + state = hass.states.get(ENTITY_ID) + assert state.name == NAME + assert state.state == STATE_ON + attributes = state.attributes + assert attributes[ATTR_STATUS] == "Cleaning" + assert attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_DYSON + assert attributes[ATTR_BATTERY_LEVEL] == 85 + assert attributes[ATTR_POSITION] == "(0, 0)" + assert attributes[ATTR_FAN_SPEED] == "Quiet" + assert attributes[ATTR_FAN_SPEED_LIST] == ["Quiet", "Max"] -def _get_vacuum_device_charging(): - """Return a vacuum device charging.""" - device = mock.Mock(spec=Dyson360Eye) - device.name = "Device_Vacuum" - device.state = mock.MagicMock() device.state.state = Dyson360EyeMode.INACTIVE_CHARGING - device.state.battery_level = 40 - device.state.power_mode = PowerMode.QUIET - device.state.position = (0, 0) - return device + device.state.power_mode = PowerMode.MAX + await async_update_device(hass, device) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF + assert state.attributes[ATTR_STATUS] == "Stopped - Charging" + assert state.attributes[ATTR_FAN_SPEED] == "Max" - -def _get_vacuum_device_pause(): - """Return a vacuum device in pause.""" - device = mock.MagicMock(spec=Dyson360Eye) - device.name = "Device_Vacuum" - device.state = mock.MagicMock() device.state.state = Dyson360EyeMode.FULL_CLEAN_PAUSED - device.state.battery_level = 40 - device.state.power_mode = PowerMode.QUIET - device.state.position = (0, 0) - return device + await async_update_device(hass, device) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_OFF + assert state.attributes[ATTR_STATUS] == "Paused" -def _get_vacuum_device_unknown_state(): - """Return a vacuum device with unknown state.""" - device = mock.Mock(spec=Dyson360Eye) - device.name = "Device_Vacuum" - device.state = mock.MagicMock() - device.state.state = "Unknown" - return device +@pytest.mark.parametrize( + "service,command,device", + [ + (SERVICE_TURN_ON, "start", Dyson360EyeMode.INACTIVE_CHARGED), + (SERVICE_TURN_ON, "resume", Dyson360EyeMode.FULL_CLEAN_PAUSED), + (SERVICE_TURN_OFF, "pause", Dyson360EyeMode.FULL_CLEAN_RUNNING), + (SERVICE_STOP, "pause", Dyson360EyeMode.FULL_CLEAN_RUNNING), + (SERVICE_START_PAUSE, "pause", Dyson360EyeMode.FULL_CLEAN_RUNNING), + (SERVICE_START_PAUSE, "pause", Dyson360EyeMode.FULL_CLEAN_RUNNING), + (SERVICE_START_PAUSE, "start", Dyson360EyeMode.INACTIVE_CHARGED), + (SERVICE_START_PAUSE, "resume", Dyson360EyeMode.FULL_CLEAN_PAUSED), + (SERVICE_RETURN_TO_BASE, "abort", Dyson360EyeMode.FULL_CLEAN_PAUSED), + ], + indirect=["device"], +) +async def test_commands( + hass: HomeAssistant, device: Dyson360Eye, service: str, command: str +) -> None: + """Test sending commands to the vacuum.""" + await hass.services.async_call( + PLATFORM_DOMAIN, service, {ATTR_ENTITY_ID: ENTITY_ID}, blocking=True + ) + getattr(device, command).assert_called_once_with() -class DysonTest(unittest.TestCase): - """Dyson 360 eye robot vacuum component test class.""" - - def setUp(self): # pylint: disable=invalid-name - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.addCleanup(self.tear_down_cleanup) - - def tear_down_cleanup(self): - """Stop everything that was started.""" - self.hass.stop() - - def test_setup_component_with_no_devices(self): - """Test setup component with no devices.""" - self.hass.data[dyson.DYSON_DEVICES] = [] - add_entities = mock.MagicMock() - dyson.setup_platform(self.hass, {}, add_entities) - add_entities.assert_called_with([]) - - def test_setup_component(self): - """Test setup component with devices.""" - - def _add_device(devices): - assert len(devices) == 1 - assert devices[0].name == "Device_Vacuum" - - device_vacuum = _get_vacuum_device_cleaning() - device_non_vacuum = _get_non_vacuum_device() - self.hass.data[dyson.DYSON_DEVICES] = [device_vacuum, device_non_vacuum] - dyson.setup_platform(self.hass, {}, _add_device) - - def test_on_message(self): - """Test when message is received.""" - device = _get_vacuum_device_cleaning() - component = Dyson360EyeDevice(device) - component.entity_id = "entity_id" - component.schedule_update_ha_state = mock.Mock() - component.on_message(mock.Mock()) - assert component.schedule_update_ha_state.called - - def test_should_poll(self): - """Test polling is disable.""" - device = _get_vacuum_device_cleaning() - component = Dyson360EyeDevice(device) - assert not component.should_poll - - def test_properties(self): - """Test component properties.""" - device1 = _get_vacuum_device_cleaning() - device2 = _get_vacuum_device_unknown_state() - device3 = _get_vacuum_device_charging() - component = Dyson360EyeDevice(device1) - component2 = Dyson360EyeDevice(device2) - component3 = Dyson360EyeDevice(device3) - assert component.name == "Device_Vacuum" - assert component.is_on - assert component.status == "Cleaning" - assert component2.status == "Unknown" - assert component.battery_level == 85 - assert component.fan_speed == "Quiet" - assert component.fan_speed_list == ["Quiet", "Max"] - assert component.device_state_attributes["position"] == "(0, 0)" - assert component.available - assert component.supported_features == 255 - assert component.battery_icon == "mdi:battery-80" - assert component3.battery_icon == "mdi:battery-charging-40" - - def test_turn_on(self): - """Test turn on vacuum.""" - device1 = _get_vacuum_device_charging() - component1 = Dyson360EyeDevice(device1) - component1.turn_on() - assert device1.start.called - - device2 = _get_vacuum_device_pause() - component2 = Dyson360EyeDevice(device2) - component2.turn_on() - assert device2.resume.called - - def test_turn_off(self): - """Test turn off vacuum.""" - device1 = _get_vacuum_device_cleaning() - component1 = Dyson360EyeDevice(device1) - component1.turn_off() - assert device1.pause.called - - def test_stop(self): - """Test stop vacuum.""" - device1 = _get_vacuum_device_cleaning() - component1 = Dyson360EyeDevice(device1) - component1.stop() - assert device1.pause.called - - def test_set_fan_speed(self): - """Test set fan speed vacuum.""" - device1 = _get_vacuum_device_cleaning() - component1 = Dyson360EyeDevice(device1) - component1.set_fan_speed("Max") - device1.set_power_mode.assert_called_with(PowerMode.MAX) - - def test_start_pause(self): - """Test start/pause.""" - device1 = _get_vacuum_device_charging() - component1 = Dyson360EyeDevice(device1) - component1.start_pause() - assert device1.start.called - - device2 = _get_vacuum_device_pause() - component2 = Dyson360EyeDevice(device2) - component2.start_pause() - assert device2.resume.called - - device3 = _get_vacuum_device_cleaning() - component3 = Dyson360EyeDevice(device3) - component3.start_pause() - assert device3.pause.called - - def test_return_to_base(self): - """Test return to base.""" - device = _get_vacuum_device_pause() - component = Dyson360EyeDevice(device) - component.return_to_base() - assert device.abort.called +async def test_set_fan_speed(hass: HomeAssistant, device: Dyson360Eye): + """Test setting fan speed of the vacuum.""" + fan_speed_map = { + "Max": PowerMode.MAX, + "Quiet": PowerMode.QUIET, + } + for service_speed, command_speed in fan_speed_map.items(): + await hass.services.async_call( + PLATFORM_DOMAIN, + SERVICE_SET_FAN_SPEED, + {ATTR_ENTITY_ID: ENTITY_ID, ATTR_FAN_SPEED: service_speed}, + blocking=True, + ) + device.set_power_mode.assert_called_with(command_speed) diff --git a/tests/components/eafm/conftest.py b/tests/components/eafm/conftest.py index 7233257f2eb..69bb2258486 100644 --- a/tests/components/eafm/conftest.py +++ b/tests/components/eafm/conftest.py @@ -1,8 +1,8 @@ """eafm fixtures.""" -import pytest +from unittest.mock import patch -from tests.async_mock import patch +import pytest @pytest.fixture() diff --git a/tests/components/eafm/test_config_flow.py b/tests/components/eafm/test_config_flow.py index cd71767104f..8a1e2bd89fc 100644 --- a/tests/components/eafm/test_config_flow.py +++ b/tests/components/eafm/test_config_flow.py @@ -1,11 +1,11 @@ """Tests for eafm config flow.""" +from unittest.mock import patch + import pytest from voluptuous.error import MultipleInvalid from homeassistant.components.eafm import const -from tests.async_mock import patch - async def test_flow_no_discovered_stations(hass, mock_get_stations): """Test config flow discovers no station.""" diff --git a/tests/components/ecobee/test_climate.py b/tests/components/ecobee/test_climate.py index 32575e7188a..a9b9165d713 100644 --- a/tests/components/ecobee/test_climate.py +++ b/tests/components/ecobee/test_climate.py @@ -208,26 +208,32 @@ async def test_set_temperature(ecobee_fixture, thermostat, data): # Auto -> Auto data.reset_mock() thermostat.set_temperature(target_temp_low=20, target_temp_high=30) - data.ecobee.set_hold_temp.assert_has_calls([mock.call(1, 30, 20, "nextTransition")]) + data.ecobee.set_hold_temp.assert_has_calls( + [mock.call(1, 30, 20, "nextTransition", 0)] + ) # Auto -> Hold data.reset_mock() thermostat.set_temperature(temperature=20) - data.ecobee.set_hold_temp.assert_has_calls([mock.call(1, 25, 15, "nextTransition")]) + data.ecobee.set_hold_temp.assert_has_calls( + [mock.call(1, 25, 15, "nextTransition", 0)] + ) # Cool -> Hold data.reset_mock() ecobee_fixture["settings"]["hvacMode"] = "cool" thermostat.set_temperature(temperature=20.5) data.ecobee.set_hold_temp.assert_has_calls( - [mock.call(1, 20.5, 20.5, "nextTransition")] + [mock.call(1, 20.5, 20.5, "nextTransition", 0)] ) # Heat -> Hold data.reset_mock() ecobee_fixture["settings"]["hvacMode"] = "heat" thermostat.set_temperature(temperature=20) - data.ecobee.set_hold_temp.assert_has_calls([mock.call(1, 20, 20, "nextTransition")]) + data.ecobee.set_hold_temp.assert_has_calls( + [mock.call(1, 20, 20, "nextTransition", 0)] + ) # Heat -> Auto data.reset_mock() @@ -280,16 +286,32 @@ async def test_resume_program(thermostat, data): async def test_hold_preference(ecobee_fixture, thermostat): """Test hold preference.""" - assert thermostat.hold_preference() == "nextTransition" + ecobee_fixture["settings"]["holdAction"] = "indefinite" + assert thermostat.hold_preference() == "indefinite" + for action in ["useEndTime2hour", "useEndTime4hour"]: + ecobee_fixture["settings"]["holdAction"] = action + assert thermostat.hold_preference() == "holdHours" + for action in [ + "nextPeriod", + "askMe", + ]: + ecobee_fixture["settings"]["holdAction"] = action + assert thermostat.hold_preference() == "nextTransition" + + +def test_hold_hours(ecobee_fixture, thermostat): + """Test hold hours preference.""" + ecobee_fixture["settings"]["holdAction"] = "useEndTime2hour" + assert thermostat.hold_hours() == 2 + ecobee_fixture["settings"]["holdAction"] = "useEndTime4hour" + assert thermostat.hold_hours() == 4 for action in [ - "useEndTime4hour", - "useEndTime2hour", "nextPeriod", "indefinite", "askMe", ]: ecobee_fixture["settings"]["holdAction"] = action - assert thermostat.hold_preference() == "nextTransition" + assert thermostat.hold_hours() == 0 async def test_set_fan_mode_on(thermostat, data): diff --git a/tests/components/econet/__init__.py b/tests/components/econet/__init__.py new file mode 100644 index 00000000000..c0f921f65d0 --- /dev/null +++ b/tests/components/econet/__init__.py @@ -0,0 +1 @@ +"""Tests for the Econet component.""" diff --git a/tests/components/econet/test_config_flow.py b/tests/components/econet/test_config_flow.py new file mode 100644 index 00000000000..68eb18a931e --- /dev/null +++ b/tests/components/econet/test_config_flow.py @@ -0,0 +1,140 @@ +"""Tests for the Econet component.""" +from unittest.mock import patch + +from pyeconet.api import EcoNetApiInterface +from pyeconet.errors import InvalidCredentialsError, PyeconetError + +from homeassistant.components.econet import DOMAIN +from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from tests.common import MockConfigEntry + + +async def test_bad_credentials(hass): + """Test when provided credentials are rejected.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == SOURCE_USER + + with patch( + "pyeconet.EcoNetApiInterface.login", + side_effect=InvalidCredentialsError(), + ), patch("homeassistant.components.econet.async_setup", return_value=True), patch( + "homeassistant.components.econet.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_EMAIL: "admin@localhost.com", + CONF_PASSWORD: "password0", + }, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == { + "base": "invalid_auth", + } + + +async def test_generic_error_from_library(hass): + """Test when connection fails.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == SOURCE_USER + + with patch( + "pyeconet.EcoNetApiInterface.login", + side_effect=PyeconetError(), + ), patch("homeassistant.components.econet.async_setup", return_value=True), patch( + "homeassistant.components.econet.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_EMAIL: "admin@localhost.com", + CONF_PASSWORD: "password0", + }, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == { + "base": "cannot_connect", + } + + +async def test_auth_worked(hass): + """Test when provided credentials are accepted.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == SOURCE_USER + + with patch( + "pyeconet.EcoNetApiInterface.login", + return_value=EcoNetApiInterface, + ), patch("homeassistant.components.econet.async_setup", return_value=True), patch( + "homeassistant.components.econet.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_EMAIL: "admin@localhost.com", + CONF_PASSWORD: "password0", + }, + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + CONF_EMAIL: "admin@localhost.com", + CONF_PASSWORD: "password0", + } + + +async def test_already_configured(hass): + """Test when provided credentials are already configured.""" + config = { + CONF_EMAIL: "admin@localhost.com", + CONF_PASSWORD: "password0", + } + MockConfigEntry( + domain=DOMAIN, data=config, unique_id="admin@localhost.com" + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == SOURCE_USER + + with patch( + "pyeconet.EcoNetApiInterface.login", + return_value=EcoNetApiInterface, + ), patch("homeassistant.components.econet.async_setup", return_value=True), patch( + "homeassistant.components.econet.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_EMAIL: "admin@localhost.com", + CONF_PASSWORD: "password0", + }, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/ee_brightbox/test_device_tracker.py b/tests/components/ee_brightbox/test_device_tracker.py index 64f24ec289c..06c3ce0cd1d 100644 --- a/tests/components/ee_brightbox/test_device_tracker.py +++ b/tests/components/ee_brightbox/test_device_tracker.py @@ -1,5 +1,6 @@ """Tests for the EE BrightBox device scanner.""" from datetime import datetime +from unittest.mock import patch from eebrightbox import EEBrightBoxException import pytest @@ -8,8 +9,6 @@ from homeassistant.components.device_tracker import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM from homeassistant.setup import async_setup_component -from tests.async_mock import patch - def _configure_mock_get_devices(eebrightbox_mock): eebrightbox_instance = eebrightbox_mock.return_value diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index 346d0b65ad2..d73bbe53e9a 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -1,10 +1,10 @@ """Test the Elk-M1 Control config flow.""" +from unittest.mock import MagicMock, patch + from homeassistant import config_entries, setup from homeassistant.components.elkm1.const import DOMAIN -from tests.async_mock import MagicMock, patch - def mock_elk(invalid_auth=None, sync_complete=None): """Mock m1lib Elk.""" diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 576a464c86a..4d8079b9db9 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -3,6 +3,7 @@ import asyncio from datetime import timedelta from ipaddress import ip_address import json +from unittest.mock import patch from aiohttp.hdrs import CONTENT_TYPE import pytest @@ -50,7 +51,6 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import async_fire_time_changed, get_test_instance_port HTTP_SERVER_PORT = get_test_instance_port() diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py index b1cf2aacb1b..6fa6d969539 100644 --- a/tests/components/emulated_hue/test_init.py +++ b/tests/components/emulated_hue/test_init.py @@ -1,7 +1,7 @@ """Test the Emulated Hue component.""" -from homeassistant.components.emulated_hue import Config +from unittest.mock import MagicMock, Mock, patch -from tests.async_mock import MagicMock, Mock, patch +from homeassistant.components.emulated_hue import Config def test_config_google_home_entity_id_to_number(): diff --git a/tests/components/emulated_kasa/test_init.py b/tests/components/emulated_kasa/test_init.py index 10ccb4d68a5..60f4f5be1db 100644 --- a/tests/components/emulated_kasa/test_init.py +++ b/tests/components/emulated_kasa/test_init.py @@ -1,5 +1,6 @@ """Tests for emulated_kasa library bindings.""" import math +from unittest.mock import AsyncMock, Mock, patch from homeassistant.components import emulated_kasa from homeassistant.components.emulated_kasa.const import ( @@ -29,8 +30,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, Mock, patch - ENTITY_SWITCH = "switch.ac" ENTITY_SWITCH_NAME = "A/C" ENTITY_SWITCH_POWER = 400.0 diff --git a/tests/components/emulated_roku/test_binding.py b/tests/components/emulated_roku/test_binding.py index 5ff29194adf..5afee6f6cc3 100644 --- a/tests/components/emulated_roku/test_binding.py +++ b/tests/components/emulated_roku/test_binding.py @@ -1,4 +1,6 @@ """Tests for emulated_roku library bindings.""" +from unittest.mock import AsyncMock, Mock, patch + from homeassistant.components.emulated_roku.binding import ( ATTR_APP_ID, ATTR_COMMAND_TYPE, @@ -12,8 +14,6 @@ from homeassistant.components.emulated_roku.binding import ( EmulatedRoku, ) -from tests.async_mock import AsyncMock, Mock, patch - async def test_events_fired_properly(hass): """Test that events are fired correctly.""" diff --git a/tests/components/emulated_roku/test_init.py b/tests/components/emulated_roku/test_init.py index 92952a5d840..8f256ee4c79 100644 --- a/tests/components/emulated_roku/test_init.py +++ b/tests/components/emulated_roku/test_init.py @@ -1,9 +1,9 @@ """Test emulated_roku component setup process.""" +from unittest.mock import AsyncMock, Mock, patch + from homeassistant.components import emulated_roku from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, Mock, patch - async def test_config_required_fields(hass): """Test that configuration is successful with required fields.""" diff --git a/tests/components/enocean/test_config_flow.py b/tests/components/enocean/test_config_flow.py index b815d48694a..4cea2a4fb9b 100644 --- a/tests/components/enocean/test_config_flow.py +++ b/tests/components/enocean/test_config_flow.py @@ -1,10 +1,11 @@ """Tests for EnOcean config flow.""" +from unittest.mock import Mock, patch + from homeassistant import data_entry_flow from homeassistant.components.enocean.config_flow import EnOceanFlowHandler from homeassistant.components.enocean.const import DOMAIN from homeassistant.const import CONF_DEVICE -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry DONGLE_VALIDATE_PATH_METHOD = "homeassistant.components.enocean.dongle.validate_path" diff --git a/tests/components/epson/test_config_flow.py b/tests/components/epson/test_config_flow.py index 6faaa285a2a..4a0b9f9675f 100644 --- a/tests/components/epson/test_config_flow.py +++ b/tests/components/epson/test_config_flow.py @@ -1,10 +1,10 @@ """Test the epson config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, setup from homeassistant.components.epson.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_UNAVAILABLE -from tests.async_mock import patch - async def test_form(hass): """Test we get the form.""" diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 41c3d6cf528..f3afce0d43b 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -1,5 +1,6 @@ """Test config flow.""" from collections import namedtuple +from unittest.mock import AsyncMock, MagicMock, patch import pytest @@ -11,7 +12,6 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) -from tests.async_mock import AsyncMock, MagicMock, patch from tests.common import MockConfigEntry MockDeviceInfo = namedtuple("DeviceInfo", ["uses_password", "name"]) diff --git a/tests/components/facebox/test_image_processing.py b/tests/components/facebox/test_image_processing.py index 00ce7af0d01..b0e482a89f5 100644 --- a/tests/components/facebox/test_image_processing.py +++ b/tests/components/facebox/test_image_processing.py @@ -1,4 +1,6 @@ """The tests for the facebox component.""" +from unittest.mock import Mock, mock_open, patch + import pytest import requests import requests_mock @@ -21,8 +23,6 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, mock_open, patch - MOCK_IP = "192.168.0.1" MOCK_PORT = "8080" diff --git a/tests/components/fail2ban/test_sensor.py b/tests/components/fail2ban/test_sensor.py index 09daee423aa..e43064c54ab 100644 --- a/tests/components/fail2ban/test_sensor.py +++ b/tests/components/fail2ban/test_sensor.py @@ -1,4 +1,6 @@ """The tests for local file sensor platform.""" +from unittest.mock import Mock, mock_open, patch + from homeassistant.components.fail2ban.sensor import ( STATE_ALL_BANS, STATE_CURRENT_BANS, @@ -7,7 +9,6 @@ from homeassistant.components.fail2ban.sensor import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, mock_open, patch from tests.common import assert_setup_component diff --git a/tests/components/feedreader/test_init.py b/tests/components/feedreader/test_init.py index b50ef5f6619..307ba577de3 100644 --- a/tests/components/feedreader/test_init.py +++ b/tests/components/feedreader/test_init.py @@ -6,6 +6,7 @@ from os.path import exists import time import unittest from unittest import mock +from unittest.mock import patch from homeassistant.components import feedreader from homeassistant.components.feedreader import ( @@ -21,7 +22,6 @@ from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START from homeassistant.core import callback from homeassistant.setup import setup_component -from tests.async_mock import patch from tests.common import get_test_home_assistant, load_fixture _LOGGER = getLogger(__name__) diff --git a/tests/components/ffmpeg/test_init.py b/tests/components/ffmpeg/test_init.py index 4187fe561cc..3c6a2fbb92d 100644 --- a/tests/components/ffmpeg/test_init.py +++ b/tests/components/ffmpeg/test_init.py @@ -1,4 +1,6 @@ """The tests for Home Assistant ffmpeg.""" +from unittest.mock import MagicMock + import homeassistant.components.ffmpeg as ffmpeg from homeassistant.components.ffmpeg import ( DOMAIN, @@ -10,7 +12,6 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import callback from homeassistant.setup import async_setup_component, setup_component -from tests.async_mock import MagicMock from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/fido/test_sensor.py b/tests/components/fido/test_sensor.py index 551a59f0788..3baaf2e350c 100644 --- a/tests/components/fido/test_sensor.py +++ b/tests/components/fido/test_sensor.py @@ -1,12 +1,12 @@ """The test for the fido sensor platform.""" import logging +from unittest.mock import MagicMock, patch from pyfido.client import PyFidoError from homeassistant.bootstrap import async_setup_component from homeassistant.components.fido import sensor as fido -from tests.async_mock import MagicMock, patch from tests.common import assert_setup_component CONTRACT = "123456789" diff --git a/tests/components/file/test_notify.py b/tests/components/file/test_notify.py index 5bb387ea1e3..d2db5d9e8a8 100644 --- a/tests/components/file/test_notify.py +++ b/tests/components/file/test_notify.py @@ -1,5 +1,6 @@ """The tests for the notify file platform.""" import os +from unittest.mock import call, mock_open, patch import pytest @@ -8,7 +9,6 @@ from homeassistant.components.notify import ATTR_TITLE_DEFAULT from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import call, mock_open, patch from tests.common import assert_setup_component diff --git a/tests/components/file/test_sensor.py b/tests/components/file/test_sensor.py index 31370334f92..99e08362ab7 100644 --- a/tests/components/file/test_sensor.py +++ b/tests/components/file/test_sensor.py @@ -1,10 +1,11 @@ """The tests for local file sensor platform.""" +from unittest.mock import Mock, mock_open, patch + import pytest from homeassistant.const import STATE_UNKNOWN from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, mock_open, patch from tests.common import mock_registry diff --git a/tests/components/filesize/test_sensor.py b/tests/components/filesize/test_sensor.py index 60f6d273ea4..5649a678e3a 100644 --- a/tests/components/filesize/test_sensor.py +++ b/tests/components/filesize/test_sensor.py @@ -1,5 +1,6 @@ """The tests for the filesize sensor.""" import os +from unittest.mock import patch import pytest @@ -9,8 +10,6 @@ from homeassistant.components.filesize.sensor import CONF_FILE_PATHS from homeassistant.const import SERVICE_RELOAD from homeassistant.setup import async_setup_component -from tests.async_mock import patch - TEST_DIR = os.path.join(os.path.dirname(__file__)) TEST_FILE = os.path.join(TEST_DIR, "mock_file_test_filesize.txt") diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py index 1c3b4b0d672..417c84e8ea4 100644 --- a/tests/components/filter/test_sensor.py +++ b/tests/components/filter/test_sensor.py @@ -1,6 +1,7 @@ """The test for the data filter sensor platform.""" from datetime import timedelta from os import path +from unittest.mock import patch from pytest import fixture @@ -20,7 +21,6 @@ import homeassistant.core as ha from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import assert_setup_component, async_init_recorder_component diff --git a/tests/components/fireservicerota/test_config_flow.py b/tests/components/fireservicerota/test_config_flow.py index 8ccaae5fbbc..69f37bc3ad4 100644 --- a/tests/components/fireservicerota/test_config_flow.py +++ b/tests/components/fireservicerota/test_config_flow.py @@ -1,11 +1,12 @@ """Test the FireServiceRota config flow.""" +from unittest.mock import patch + from pyfireservicerota import InvalidAuthError from homeassistant import data_entry_flow from homeassistant.components.fireservicerota.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry MOCK_CONF = { diff --git a/tests/components/firmata/test_config_flow.py b/tests/components/firmata/test_config_flow.py index e77f219e320..91db94052cc 100644 --- a/tests/components/firmata/test_config_flow.py +++ b/tests/components/firmata/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Firmata config flow.""" +from unittest.mock import patch + from pymata_express.pymata_express_serial import serial from homeassistant import config_entries, setup @@ -6,8 +8,6 @@ from homeassistant.components.firmata.const import CONF_SERIAL_PORT, DOMAIN from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant -from tests.async_mock import patch - async def test_import_cannot_connect_pymata(hass: HomeAssistant) -> None: """Test we fail with an invalid board.""" diff --git a/tests/components/flick_electric/test_config_flow.py b/tests/components/flick_electric/test_config_flow.py index f18daed875f..1890ea9448a 100644 --- a/tests/components/flick_electric/test_config_flow.py +++ b/tests/components/flick_electric/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Flick Electric config flow.""" import asyncio +from unittest.mock import patch from pyflick.authentication import AuthException @@ -7,7 +8,6 @@ from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.flick_electric.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry CONF = {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"} diff --git a/tests/components/flo/test_config_flow.py b/tests/components/flo/test_config_flow.py index edc9705b7cd..d26051bbcb2 100644 --- a/tests/components/flo/test_config_flow.py +++ b/tests/components/flo/test_config_flow.py @@ -1,6 +1,7 @@ """Test the flo config flow.""" import json import time +from unittest.mock import patch from homeassistant import config_entries, setup from homeassistant.components.flo.const import DOMAIN @@ -8,8 +9,6 @@ from homeassistant.const import CONTENT_TYPE_JSON from .common import TEST_EMAIL_ADDRESS, TEST_PASSWORD, TEST_TOKEN, TEST_USER_ID -from tests.async_mock import patch - async def test_form(hass, aioclient_mock_fixture): """Test we get the form.""" diff --git a/tests/components/flume/test_config_flow.py b/tests/components/flume/test_config_flow.py index 0afea0a9742..9ae0889d52c 100644 --- a/tests/components/flume/test_config_flow.py +++ b/tests/components/flume/test_config_flow.py @@ -1,4 +1,6 @@ """Test the flume config flow.""" +from unittest.mock import MagicMock, patch + import requests.exceptions from homeassistant import config_entries, setup @@ -10,8 +12,6 @@ from homeassistant.const import ( CONF_USERNAME, ) -from tests.async_mock import MagicMock, patch - def _get_mocked_flume_device_list(): flume_device_list_mock = MagicMock() diff --git a/tests/components/flunearyou/test_config_flow.py b/tests/components/flunearyou/test_config_flow.py index 3681768ccdf..fbed4d2b426 100644 --- a/tests/components/flunearyou/test_config_flow.py +++ b/tests/components/flunearyou/test_config_flow.py @@ -1,4 +1,6 @@ """Define tests for the flunearyou config flow.""" +from unittest.mock import patch + from pyflunearyou.errors import FluNearYouError from homeassistant import data_entry_flow @@ -6,7 +8,6 @@ from homeassistant.components.flunearyou import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/flux/test_switch.py b/tests/components/flux/test_switch.py index be1fcf4c5ee..0d4e4f0595e 100644 --- a/tests/components/flux/test_switch.py +++ b/tests/components/flux/test_switch.py @@ -1,4 +1,6 @@ """The tests for the Flux switch platform.""" +from unittest.mock import patch + import pytest from homeassistant.components import light, switch @@ -13,7 +15,6 @@ from homeassistant.core import State from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import ( assert_setup_component, async_fire_time_changed, diff --git a/tests/components/folder_watcher/test_init.py b/tests/components/folder_watcher/test_init.py index 1123a5907fb..b0a522cb7fc 100644 --- a/tests/components/folder_watcher/test_init.py +++ b/tests/components/folder_watcher/test_init.py @@ -1,11 +1,10 @@ """The tests for the folder_watcher component.""" import os +from unittest.mock import Mock, patch from homeassistant.components import folder_watcher from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch - async def test_invalid_path_setup(hass): """Test that an invalid path is not set up.""" diff --git a/tests/components/foobot/test_sensor.py b/tests/components/foobot/test_sensor.py index 9b8a81c96d5..f817b38c98b 100644 --- a/tests/components/foobot/test_sensor.py +++ b/tests/components/foobot/test_sensor.py @@ -2,6 +2,7 @@ import asyncio import re +from unittest.mock import MagicMock import pytest @@ -19,7 +20,6 @@ from homeassistant.const import ( from homeassistant.exceptions import PlatformNotReady from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock from tests.common import load_fixture VALID_CONFIG = { diff --git a/tests/components/forked_daapd/test_config_flow.py b/tests/components/forked_daapd/test_config_flow.py index f655e727667..843aa12a759 100644 --- a/tests/components/forked_daapd/test_config_flow.py +++ b/tests/components/forked_daapd/test_config_flow.py @@ -1,4 +1,6 @@ """The config flow tests for the forked_daapd media player platform.""" +from unittest.mock import AsyncMock, patch + import pytest from homeassistant import data_entry_flow @@ -16,7 +18,6 @@ from homeassistant.config_entries import ( ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT -from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry SAMPLE_CONFIG = { diff --git a/tests/components/forked_daapd/test_media_player.py b/tests/components/forked_daapd/test_media_player.py index 15755949062..149cbdae4e2 100644 --- a/tests/components/forked_daapd/test_media_player.py +++ b/tests/components/forked_daapd/test_media_player.py @@ -1,5 +1,7 @@ """The media player tests for the forked_daapd media player platform.""" +from unittest.mock import patch + import pytest from homeassistant.components.forked_daapd.const import ( @@ -65,7 +67,6 @@ from homeassistant.const import ( STATE_UNAVAILABLE, ) -from tests.async_mock import patch from tests.common import MockConfigEntry, async_mock_signal TEST_MASTER_ENTITY_NAME = "media_player.forked_daapd_server" diff --git a/tests/components/foscam/__init__.py b/tests/components/foscam/__init__.py new file mode 100644 index 00000000000..391907b8a8c --- /dev/null +++ b/tests/components/foscam/__init__.py @@ -0,0 +1 @@ +"""Tests for the Foscam integration.""" diff --git a/tests/components/foscam/test_config_flow.py b/tests/components/foscam/test_config_flow.py new file mode 100644 index 00000000000..8087ac1894f --- /dev/null +++ b/tests/components/foscam/test_config_flow.py @@ -0,0 +1,358 @@ +"""Test the Foscam config flow.""" +from unittest.mock import patch + +from libpyfoscam.foscam import ERROR_FOSCAM_AUTH, ERROR_FOSCAM_UNAVAILABLE + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.foscam import config_flow + +from tests.common import MockConfigEntry + +VALID_CONFIG = { + config_flow.CONF_HOST: "10.0.0.2", + config_flow.CONF_PORT: 88, + config_flow.CONF_USERNAME: "admin", + config_flow.CONF_PASSWORD: "1234", + config_flow.CONF_STREAM: "Main", +} +CAMERA_NAME = "Mocked Foscam Camera" +CAMERA_MAC = "C0:C1:D0:F4:B4:D4" + + +def setup_mock_foscam_camera(mock_foscam_camera): + """Mock FoscamCamera simulating behaviour using a base valid config.""" + + def configure_mock_on_init(host, port, user, passwd, verbose=False): + return_code = 0 + data = {} + + if ( + host != VALID_CONFIG[config_flow.CONF_HOST] + or port != VALID_CONFIG[config_flow.CONF_PORT] + ): + return_code = ERROR_FOSCAM_UNAVAILABLE + + elif ( + user != VALID_CONFIG[config_flow.CONF_USERNAME] + or passwd != VALID_CONFIG[config_flow.CONF_PASSWORD] + ): + return_code = ERROR_FOSCAM_AUTH + + else: + data["devName"] = CAMERA_NAME + data["mac"] = CAMERA_MAC + + mock_foscam_camera.get_dev_info.return_value = (return_code, data) + + return mock_foscam_camera + + mock_foscam_camera.side_effect = configure_mock_on_init + + +async def test_user_valid(hass): + """Test valid config from user input.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.foscam.config_flow.FoscamCamera", + ) as mock_foscam_camera, patch( + "homeassistant.components.foscam.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.foscam.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + setup_mock_foscam_camera(mock_foscam_camera) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + VALID_CONFIG, + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == CAMERA_NAME + assert result["data"] == VALID_CONFIG + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_user_invalid_auth(hass): + """Test we handle invalid auth from user input.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.foscam.config_flow.FoscamCamera", + ) as mock_foscam_camera: + setup_mock_foscam_camera(mock_foscam_camera) + + invalid_user = VALID_CONFIG.copy() + invalid_user[config_flow.CONF_USERNAME] = "invalid" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + invalid_user, + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_user_cannot_connect(hass): + """Test we handle cannot connect error from user input.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.foscam.config_flow.FoscamCamera", + ) as mock_foscam_camera: + setup_mock_foscam_camera(mock_foscam_camera) + + invalid_host = VALID_CONFIG.copy() + invalid_host[config_flow.CONF_HOST] = "127.0.0.1" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + invalid_host, + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_user_already_configured(hass): + """Test we handle already configured from user input.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry = MockConfigEntry( + domain=config_flow.DOMAIN, data=VALID_CONFIG, unique_id=CAMERA_MAC + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.foscam.config_flow.FoscamCamera", + ) as mock_foscam_camera: + setup_mock_foscam_camera(mock_foscam_camera) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + VALID_CONFIG, + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_user_unknown_exception(hass): + """Test we handle unknown exceptions from user input.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.foscam.config_flow.FoscamCamera", + ) as mock_foscam_camera: + mock_foscam_camera.side_effect = Exception("test") + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + VALID_CONFIG, + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "unknown"} + + +async def test_import_user_valid(hass): + """Test valid config from import.""" + with patch( + "homeassistant.components.foscam.config_flow.FoscamCamera", + ) as mock_foscam_camera, patch( + "homeassistant.components.foscam.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.foscam.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + setup_mock_foscam_camera(mock_foscam_camera) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=VALID_CONFIG, + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == CAMERA_NAME + assert result["data"] == VALID_CONFIG + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_user_valid_with_name(hass): + """Test valid config with extra name from import.""" + with patch( + "homeassistant.components.foscam.config_flow.FoscamCamera", + ) as mock_foscam_camera, patch( + "homeassistant.components.foscam.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.foscam.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + setup_mock_foscam_camera(mock_foscam_camera) + + name = CAMERA_NAME + " 1234" + with_name = VALID_CONFIG.copy() + with_name[config_flow.CONF_NAME] = name + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=with_name, + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == name + assert result["data"] == VALID_CONFIG + + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_import_invalid_auth(hass): + """Test we handle invalid auth from import.""" + entry = MockConfigEntry( + domain=config_flow.DOMAIN, data=VALID_CONFIG, unique_id=CAMERA_MAC + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.foscam.config_flow.FoscamCamera", + ) as mock_foscam_camera: + setup_mock_foscam_camera(mock_foscam_camera) + + invalid_user = VALID_CONFIG.copy() + invalid_user[config_flow.CONF_USERNAME] = "invalid" + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=invalid_user, + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "invalid_auth" + + +async def test_import_cannot_connect(hass): + """Test we handle invalid auth from import.""" + entry = MockConfigEntry( + domain=config_flow.DOMAIN, data=VALID_CONFIG, unique_id=CAMERA_MAC + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.foscam.config_flow.FoscamCamera", + ) as mock_foscam_camera: + setup_mock_foscam_camera(mock_foscam_camera) + + invalid_host = VALID_CONFIG.copy() + invalid_host[config_flow.CONF_HOST] = "127.0.0.1" + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=invalid_host, + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + +async def test_import_already_configured(hass): + """Test we handle already configured from import.""" + entry = MockConfigEntry( + domain=config_flow.DOMAIN, data=VALID_CONFIG, unique_id=CAMERA_MAC + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.foscam.config_flow.FoscamCamera", + ) as mock_foscam_camera: + setup_mock_foscam_camera(mock_foscam_camera) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=VALID_CONFIG, + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_import_unknown_exception(hass): + """Test we handle unknown exceptions from import.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "homeassistant.components.foscam.config_flow.FoscamCamera", + ) as mock_foscam_camera: + mock_foscam_camera.side_effect = Exception("test") + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=VALID_CONFIG, + ) + + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "unknown" diff --git a/tests/components/freebox/conftest.py b/tests/components/freebox/conftest.py index 7581b03ce72..e813469cbbf 100644 --- a/tests/components/freebox/conftest.py +++ b/tests/components/freebox/conftest.py @@ -1,7 +1,7 @@ """Test helpers for Freebox.""" -import pytest +from unittest.mock import patch -from tests.async_mock import patch +import pytest @pytest.fixture(autouse=True) diff --git a/tests/components/freebox/test_config_flow.py b/tests/components/freebox/test_config_flow.py index addb1762df0..f7150df7efc 100644 --- a/tests/components/freebox/test_config_flow.py +++ b/tests/components/freebox/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for the Freebox config flow.""" +from unittest.mock import AsyncMock, patch + from aiofreepybox.exceptions import ( AuthorizationError, HttpRequestError, @@ -11,7 +13,6 @@ from homeassistant.components.freebox.const import DOMAIN from homeassistant.config_entries import SOURCE_DISCOVERY, SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_PORT -from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry HOST = "myrouter.freeboxos.fr" diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index 066b9a30cb3..f19e05b84df 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -1,9 +1,9 @@ """Tests for the AVM Fritz!Box integration.""" +from unittest.mock import Mock + from homeassistant.components.fritzbox.const import DOMAIN from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import Mock - MOCK_CONFIG = { DOMAIN: { CONF_DEVICES: [ diff --git a/tests/components/fritzbox/conftest.py b/tests/components/fritzbox/conftest.py index 7dcee138382..591c1037525 100644 --- a/tests/components/fritzbox/conftest.py +++ b/tests/components/fritzbox/conftest.py @@ -1,7 +1,7 @@ """Fixtures for the AVM Fritz!Box integration.""" -import pytest +from unittest.mock import Mock, patch -from tests.async_mock import Mock, patch +import pytest @pytest.fixture(name="fritz") diff --git a/tests/components/fritzbox/test_binary_sensor.py b/tests/components/fritzbox/test_binary_sensor.py index b3157a3be33..89c1dea1704 100644 --- a/tests/components/fritzbox/test_binary_sensor.py +++ b/tests/components/fritzbox/test_binary_sensor.py @@ -1,6 +1,7 @@ """Tests for AVM Fritz!Box binary sensor component.""" from datetime import timedelta from unittest import mock +from unittest.mock import Mock from requests.exceptions import HTTPError @@ -18,7 +19,6 @@ import homeassistant.util.dt as dt_util from . import MOCK_CONFIG, FritzDeviceBinarySensorMock -from tests.async_mock import Mock from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake_name" diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index 519e3afa31a..627eae5da91 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -1,5 +1,6 @@ """Tests for AVM Fritz!Box climate component.""" from datetime import timedelta +from unittest.mock import Mock, call from requests.exceptions import HTTPError @@ -41,7 +42,6 @@ import homeassistant.util.dt as dt_util from . import MOCK_CONFIG, FritzDeviceClimateMock -from tests.async_mock import Mock, call from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake_name" diff --git a/tests/components/fritzbox/test_config_flow.py b/tests/components/fritzbox/test_config_flow.py index 41396735441..31a9f89ce48 100644 --- a/tests/components/fritzbox/test_config_flow.py +++ b/tests/components/fritzbox/test_config_flow.py @@ -1,5 +1,6 @@ """Tests for AVM Fritz!Box config flow.""" from unittest import mock +from unittest.mock import Mock, patch from pyfritzhome import LoginError import pytest @@ -16,8 +17,6 @@ from homeassistant.helpers.typing import HomeAssistantType from . import MOCK_CONFIG -from tests.async_mock import Mock, patch - MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0] MOCK_SSDP_DATA = { ATTR_SSDP_LOCATION: "https://fake_host:12345/test", diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index 55dab3626db..11067c1aa51 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -1,4 +1,6 @@ """Tests for the AVM Fritz!Box integration.""" +from unittest.mock import Mock, call + from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED @@ -8,7 +10,6 @@ from homeassistant.setup import async_setup_component from . import MOCK_CONFIG, FritzDeviceSwitchMock -from tests.async_mock import Mock, call from tests.common import MockConfigEntry diff --git a/tests/components/fritzbox/test_sensor.py b/tests/components/fritzbox/test_sensor.py index 7f97e8abfb1..6dde22f074e 100644 --- a/tests/components/fritzbox/test_sensor.py +++ b/tests/components/fritzbox/test_sensor.py @@ -1,5 +1,6 @@ """Tests for AVM Fritz!Box sensor component.""" from datetime import timedelta +from unittest.mock import Mock from requests.exceptions import HTTPError @@ -20,7 +21,6 @@ import homeassistant.util.dt as dt_util from . import MOCK_CONFIG, FritzDeviceSensorMock -from tests.async_mock import Mock from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake_name" diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index c9e05b2d481..1c0f7b3f37a 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -1,5 +1,6 @@ """Tests for AVM Fritz!Box switch component.""" from datetime import timedelta +from unittest.mock import Mock from requests.exceptions import HTTPError @@ -28,7 +29,6 @@ import homeassistant.util.dt as dt_util from . import MOCK_CONFIG, FritzDeviceSwitchMock -from tests.async_mock import Mock from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake_name" diff --git a/tests/components/fritzbox_callmonitor/__init__.py b/tests/components/fritzbox_callmonitor/__init__.py new file mode 100644 index 00000000000..1afe7cb5eac --- /dev/null +++ b/tests/components/fritzbox_callmonitor/__init__.py @@ -0,0 +1 @@ +"""Tests for fritzbox_callmonitor.""" diff --git a/tests/components/fritzbox_callmonitor/test_config_flow.py b/tests/components/fritzbox_callmonitor/test_config_flow.py new file mode 100644 index 00000000000..00bc1e18679 --- /dev/null +++ b/tests/components/fritzbox_callmonitor/test_config_flow.py @@ -0,0 +1,358 @@ +"""Tests for fritzbox_callmonitor config flow.""" +from unittest.mock import PropertyMock + +from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError +from requests.exceptions import ConnectionError as RequestsConnectionError + +from homeassistant.components.fritzbox_callmonitor.config_flow import ( + RESULT_INSUFFICIENT_PERMISSIONS, + RESULT_INVALID_AUTH, + RESULT_MALFORMED_PREFIXES, + RESULT_NO_DEVIES_FOUND, +) +from homeassistant.components.fritzbox_callmonitor.const import ( + CONF_PHONEBOOK, + CONF_PREFIXES, + DOMAIN, + FRITZ_ATTR_NAME, + FRITZ_ATTR_SERIAL_NUMBER, + SERIAL_NUMBER, +) +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from tests.common import MockConfigEntry, patch + +MOCK_HOST = "fake_host" +MOCK_PORT = 1234 +MOCK_USERNAME = "fake_username" +MOCK_PASSWORD = "fake_password" +MOCK_PHONEBOOK_NAME_1 = "fake_phonebook_name_1" +MOCK_PHONEBOOK_NAME_2 = "fake_phonebook_name_2" +MOCK_PHONEBOOK_ID = 0 +MOCK_SERIAL_NUMBER = "fake_serial_number" +MOCK_NAME = "fake_call_monitor_name" + +MOCK_USER_DATA = { + CONF_HOST: MOCK_HOST, + CONF_PORT: MOCK_PORT, + CONF_PASSWORD: MOCK_PASSWORD, + CONF_USERNAME: MOCK_USERNAME, +} +MOCK_CONFIG_ENTRY = { + CONF_HOST: MOCK_HOST, + CONF_PORT: MOCK_PORT, + CONF_PASSWORD: MOCK_PASSWORD, + CONF_USERNAME: MOCK_USERNAME, + CONF_PREFIXES: None, + CONF_PHONEBOOK: MOCK_PHONEBOOK_ID, + SERIAL_NUMBER: MOCK_SERIAL_NUMBER, +} +MOCK_YAML_CONFIG = { + CONF_HOST: MOCK_HOST, + CONF_PORT: MOCK_PORT, + CONF_PASSWORD: MOCK_PASSWORD, + CONF_USERNAME: MOCK_USERNAME, + CONF_PHONEBOOK: MOCK_PHONEBOOK_ID, + CONF_NAME: MOCK_NAME, +} +MOCK_DEVICE_INFO = {FRITZ_ATTR_SERIAL_NUMBER: MOCK_SERIAL_NUMBER} +MOCK_PHONEBOOK_INFO_1 = {FRITZ_ATTR_NAME: MOCK_PHONEBOOK_NAME_1} +MOCK_PHONEBOOK_INFO_2 = {FRITZ_ATTR_NAME: MOCK_PHONEBOOK_NAME_2} +MOCK_UNIQUE_ID = f"{MOCK_SERIAL_NUMBER}-{MOCK_PHONEBOOK_ID}" + + +async def test_yaml_import(hass: HomeAssistant) -> None: + """Test configuration.yaml import.""" + + with patch( + "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.__init__", + return_value=None, + ), patch( + "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.phonebook_ids", + new_callable=PropertyMock, + return_value=[0], + ), patch( + "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.phonebook_info", + return_value=MOCK_PHONEBOOK_INFO_1, + ), patch( + "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.modelname", + return_value=MOCK_PHONEBOOK_NAME_1, + ), patch( + "homeassistant.components.fritzbox_callmonitor.config_flow.FritzConnection.__init__", + return_value=None, + ), patch( + "homeassistant.components.fritzbox_callmonitor.config_flow.FritzConnection.call_action", + return_value=MOCK_DEVICE_INFO, + ), patch( + "homeassistant.components.fritzbox_callmonitor.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=MOCK_YAML_CONFIG, + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == MOCK_NAME + assert result["data"] == MOCK_CONFIG_ENTRY + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_setup_one_phonebook(hass: HomeAssistant) -> None: + """Test setting up manually.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.__init__", + return_value=None, + ), patch( + "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.phonebook_ids", + new_callable=PropertyMock, + return_value=[0], + ), patch( + "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.phonebook_info", + return_value=MOCK_PHONEBOOK_INFO_1, + ), patch( + "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.modelname", + return_value=MOCK_PHONEBOOK_NAME_1, + ), patch( + "homeassistant.components.fritzbox_callmonitor.config_flow.FritzConnection.__init__", + return_value=None, + ), patch( + "homeassistant.components.fritzbox_callmonitor.config_flow.FritzConnection.call_action", + return_value=MOCK_DEVICE_INFO, + ), patch( + "homeassistant.components.fritzbox_callmonitor.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == MOCK_PHONEBOOK_NAME_1 + assert result["data"] == MOCK_CONFIG_ENTRY + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_setup_multiple_phonebooks(hass: HomeAssistant) -> None: + """Test setting up manually.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.__init__", + return_value=None, + ), patch( + "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.phonebook_ids", + new_callable=PropertyMock, + return_value=[0, 1], + ), patch( + "homeassistant.components.fritzbox_callmonitor.config_flow.FritzConnection.__init__", + return_value=None, + ), patch( + "homeassistant.components.fritzbox_callmonitor.config_flow.FritzConnection.call_action", + return_value=MOCK_DEVICE_INFO, + ), patch( + "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.phonebook_info", + side_effect=[MOCK_PHONEBOOK_INFO_1, MOCK_PHONEBOOK_INFO_2], + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "phonebook" + assert result["errors"] == {} + + with patch( + "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.modelname", + return_value=MOCK_PHONEBOOK_NAME_1, + ), patch( + "homeassistant.components.fritzbox_callmonitor.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PHONEBOOK: MOCK_PHONEBOOK_NAME_2}, + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == MOCK_PHONEBOOK_NAME_2 + assert result["data"] == { + CONF_HOST: MOCK_HOST, + CONF_PORT: MOCK_PORT, + CONF_PASSWORD: MOCK_PASSWORD, + CONF_USERNAME: MOCK_USERNAME, + CONF_PREFIXES: None, + CONF_PHONEBOOK: 1, + SERIAL_NUMBER: MOCK_SERIAL_NUMBER, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_setup_cannot_connect(hass: HomeAssistant) -> None: + """Test we handle cannot connect.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + + with patch( + "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.__init__", + side_effect=RequestsConnectionError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == RESULT_NO_DEVIES_FOUND + + +async def test_setup_insufficient_permissions(hass: HomeAssistant) -> None: + """Test we handle insufficient permissions.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + + with patch( + "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.__init__", + side_effect=FritzSecurityError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == RESULT_INSUFFICIENT_PERMISSIONS + + +async def test_setup_invalid_auth(hass: HomeAssistant) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + + with patch( + "homeassistant.components.fritzbox_callmonitor.base.FritzPhonebook.__init__", + side_effect=FritzConnectionException, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=MOCK_USER_DATA + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": RESULT_INVALID_AUTH} + + +async def test_options_flow_correct_prefixes(hass: HomeAssistant) -> None: + """Test config flow options.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=MOCK_UNIQUE_ID, + data=MOCK_CONFIG_ENTRY, + options={CONF_PREFIXES: None}, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.fritzbox_callmonitor.async_setup_entry", + return_value=True, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_PREFIXES: "+49, 491234"} + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == {CONF_PREFIXES: ["+49", "491234"]} + + +async def test_options_flow_incorrect_prefixes(hass: HomeAssistant) -> None: + """Test config flow options.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=MOCK_UNIQUE_ID, + data=MOCK_CONFIG_ENTRY, + options={CONF_PREFIXES: None}, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.fritzbox_callmonitor.async_setup_entry", + return_value=True, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_PREFIXES: ""} + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": RESULT_MALFORMED_PREFIXES} + + +async def test_options_flow_no_prefixes(hass: HomeAssistant) -> None: + """Test config flow options.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=MOCK_UNIQUE_ID, + data=MOCK_CONFIG_ENTRY, + options={CONF_PREFIXES: None}, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.fritzbox_callmonitor.async_setup_entry", + return_value=True, + ): + await hass.config_entries.async_setup(config_entry.entry_id) + result = await hass.config_entries.options.async_init(config_entry.entry_id) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == {CONF_PREFIXES: None} diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index 7802ee60e8c..5ae8d707cb1 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -1,6 +1,7 @@ """The tests for Home Assistant frontend.""" from datetime import timedelta import re +from unittest.mock import patch import pytest @@ -19,7 +20,6 @@ from homeassistant.loader import async_get_integration from homeassistant.setup import async_setup_component from homeassistant.util import dt -from tests.async_mock import patch from tests.common import async_capture_events, async_fire_time_changed CONFIG_THEMES = { diff --git a/tests/components/garmin_connect/test_config_flow.py b/tests/components/garmin_connect/test_config_flow.py index 93b24269441..75146570d55 100644 --- a/tests/components/garmin_connect/test_config_flow.py +++ b/tests/components/garmin_connect/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Garmin Connect config flow.""" +from unittest.mock import patch + from garminconnect import ( GarminConnectAuthenticationError, GarminConnectConnectionError, @@ -10,7 +12,6 @@ from homeassistant import data_entry_flow from homeassistant.components.garmin_connect.const import DOMAIN from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry MOCK_CONF = { diff --git a/tests/components/gdacs/__init__.py b/tests/components/gdacs/__init__.py index 648ab08507b..6e61b86dbb7 100644 --- a/tests/components/gdacs/__init__.py +++ b/tests/components/gdacs/__init__.py @@ -1,5 +1,5 @@ """Tests for the GDACS component.""" -from tests.async_mock import MagicMock +from unittest.mock import MagicMock def _generate_mock_feed_entry( diff --git a/tests/components/gdacs/test_config_flow.py b/tests/components/gdacs/test_config_flow.py index 10e4312eb38..e2ecd3902d5 100644 --- a/tests/components/gdacs/test_config_flow.py +++ b/tests/components/gdacs/test_config_flow.py @@ -1,5 +1,6 @@ """Define tests for the GDACS config flow.""" from datetime import timedelta +from unittest.mock import patch import pytest @@ -12,8 +13,6 @@ from homeassistant.const import ( CONF_SCAN_INTERVAL, ) -from tests.async_mock import patch - @pytest.fixture(name="gdacs_setup", autouse=True) def gdacs_setup_fixture(): diff --git a/tests/components/gdacs/test_geo_location.py b/tests/components/gdacs/test_geo_location.py index 4185a7f656e..7dc23eaa5d8 100644 --- a/tests/components/gdacs/test_geo_location.py +++ b/tests/components/gdacs/test_geo_location.py @@ -1,5 +1,6 @@ """The tests for the GDACS Feed integration.""" import datetime +from unittest.mock import patch from homeassistant.components import gdacs from homeassistant.components.gdacs import DEFAULT_SCAN_INTERVAL, DOMAIN, FEED @@ -33,7 +34,6 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM -from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.gdacs import _generate_mock_feed_entry diff --git a/tests/components/gdacs/test_init.py b/tests/components/gdacs/test_init.py index c0ac83ebcc2..cf78faf729b 100644 --- a/tests/components/gdacs/test_init.py +++ b/tests/components/gdacs/test_init.py @@ -1,7 +1,7 @@ """Define tests for the GDACS general setup.""" -from homeassistant.components.gdacs import DOMAIN, FEED +from unittest.mock import patch -from tests.async_mock import patch +from homeassistant.components.gdacs import DOMAIN, FEED async def test_component_unload_config_entry(hass, config_entry): diff --git a/tests/components/gdacs/test_sensor.py b/tests/components/gdacs/test_sensor.py index b123021a7e3..ac53d88478b 100644 --- a/tests/components/gdacs/test_sensor.py +++ b/tests/components/gdacs/test_sensor.py @@ -1,4 +1,6 @@ """The tests for the GDACS Feed integration.""" +from unittest.mock import patch + from homeassistant.components import gdacs from homeassistant.components.gdacs import DEFAULT_SCAN_INTERVAL from homeassistant.components.gdacs.sensor import ( @@ -18,7 +20,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.gdacs import _generate_mock_feed_entry diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index 7be1670dd4c..9a147995541 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -1,6 +1,7 @@ """The tests for generic camera component.""" import asyncio from os import path +from unittest.mock import patch from homeassistant import config as hass_config from homeassistant.components.generic import DOMAIN @@ -12,8 +13,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import patch - async def test_fetching_url(aioclient_mock, hass, hass_client): """Test that it fetches the given url.""" diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index 71c6f41282b..201ed0130ff 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -1,6 +1,7 @@ """The tests for the generic_thermostat.""" import datetime from os import path +from unittest.mock import patch import pytest import pytz @@ -37,7 +38,6 @@ from homeassistant.core import DOMAIN as HASS_DOMAIN, CoreState, State, callback from homeassistant.setup import async_setup_component from homeassistant.util.unit_system import METRIC_SYSTEM -from tests.async_mock import patch from tests.common import ( assert_setup_component, async_fire_time_changed, diff --git a/tests/components/geo_json_events/test_geo_location.py b/tests/components/geo_json_events/test_geo_location.py index d0505cc68d9..75f41bb93c0 100644 --- a/tests/components/geo_json_events/test_geo_location.py +++ b/tests/components/geo_json_events/test_geo_location.py @@ -1,4 +1,6 @@ """The tests for the geojson platform.""" +from unittest.mock import MagicMock, call, patch + from homeassistant.components import geo_location from homeassistant.components.geo_json_events.geo_location import ( ATTR_EXTERNAL_ID, @@ -21,7 +23,6 @@ from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import MagicMock, call, patch from tests.common import assert_setup_component, async_fire_time_changed URL = "http://geo.json.local/geo_json_events.json" diff --git a/tests/components/geo_rss_events/test_sensor.py b/tests/components/geo_rss_events/test_sensor.py index 8c55b8ad4dd..ead81e5c5a8 100644 --- a/tests/components/geo_rss_events/test_sensor.py +++ b/tests/components/geo_rss_events/test_sensor.py @@ -1,4 +1,6 @@ """The test for the geo rss events sensor platform.""" +from unittest.mock import MagicMock, patch + import pytest from homeassistant.components import sensor @@ -12,7 +14,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import MagicMock, patch from tests.common import assert_setup_component, async_fire_time_changed URL = "http://geo.rss.local/geo_rss_events.xml" diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index 21b6830e7f4..b87b201a144 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -1,4 +1,7 @@ """The tests for the Geofency device tracker platform.""" +# pylint: disable=redefined-outer-name +from unittest.mock import patch + import pytest from homeassistant import data_entry_flow @@ -16,9 +19,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component from homeassistant.util import slugify -# pylint: disable=redefined-outer-name -from tests.async_mock import patch - HOME_LATITUDE = 37.239622 HOME_LONGITUDE = -115.815811 diff --git a/tests/components/geonetnz_quakes/__init__.py b/tests/components/geonetnz_quakes/__init__.py index 82cb62b3939..424c6372ea8 100644 --- a/tests/components/geonetnz_quakes/__init__.py +++ b/tests/components/geonetnz_quakes/__init__.py @@ -1,5 +1,5 @@ """Tests for the geonetnz_quakes component.""" -from tests.async_mock import MagicMock +from unittest.mock import MagicMock def _generate_mock_feed_entry( diff --git a/tests/components/geonetnz_quakes/test_config_flow.py b/tests/components/geonetnz_quakes/test_config_flow.py index a4b1d9c792b..d362e9cdf0f 100644 --- a/tests/components/geonetnz_quakes/test_config_flow.py +++ b/tests/components/geonetnz_quakes/test_config_flow.py @@ -1,5 +1,6 @@ """Define tests for the GeoNet NZ Quakes config flow.""" from datetime import timedelta +from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.components.geonetnz_quakes import ( @@ -15,8 +16,6 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM, ) -from tests.async_mock import patch - async def test_duplicate_error(hass, config_entry): """Test that errors are shown when duplicates are added.""" diff --git a/tests/components/geonetnz_quakes/test_geo_location.py b/tests/components/geonetnz_quakes/test_geo_location.py index 2622cd100b3..b0e54e89929 100644 --- a/tests/components/geonetnz_quakes/test_geo_location.py +++ b/tests/components/geonetnz_quakes/test_geo_location.py @@ -1,5 +1,6 @@ """The tests for the GeoNet NZ Quakes Feed integration.""" import datetime +from unittest.mock import patch from homeassistant.components import geonetnz_quakes from homeassistant.components.geo_location import ATTR_SOURCE @@ -29,7 +30,6 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM -from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.geonetnz_quakes import _generate_mock_feed_entry diff --git a/tests/components/geonetnz_quakes/test_init.py b/tests/components/geonetnz_quakes/test_init.py index 87f2f2a7947..e8a1dc1e380 100644 --- a/tests/components/geonetnz_quakes/test_init.py +++ b/tests/components/geonetnz_quakes/test_init.py @@ -1,7 +1,7 @@ """Define tests for the GeoNet NZ Quakes general setup.""" -from homeassistant.components.geonetnz_quakes import DOMAIN, FEED +from unittest.mock import patch -from tests.async_mock import patch +from homeassistant.components.geonetnz_quakes import DOMAIN, FEED async def test_component_unload_config_entry(hass, config_entry): diff --git a/tests/components/geonetnz_quakes/test_sensor.py b/tests/components/geonetnz_quakes/test_sensor.py index 5a6e675471a..8226fd91898 100644 --- a/tests/components/geonetnz_quakes/test_sensor.py +++ b/tests/components/geonetnz_quakes/test_sensor.py @@ -1,5 +1,6 @@ """The tests for the GeoNet NZ Quakes Feed integration.""" import datetime +from unittest.mock import patch from homeassistant.components import geonetnz_quakes from homeassistant.components.geonetnz_quakes import DEFAULT_SCAN_INTERVAL @@ -20,7 +21,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.geonetnz_quakes import _generate_mock_feed_entry diff --git a/tests/components/geonetnz_volcano/__init__.py b/tests/components/geonetnz_volcano/__init__.py index 023cab46ec8..708b69e0031 100644 --- a/tests/components/geonetnz_volcano/__init__.py +++ b/tests/components/geonetnz_volcano/__init__.py @@ -1,5 +1,5 @@ """The tests for the GeoNet NZ Volcano Feed integration.""" -from tests.async_mock import MagicMock +from unittest.mock import MagicMock def _generate_mock_feed_entry( diff --git a/tests/components/geonetnz_volcano/test_config_flow.py b/tests/components/geonetnz_volcano/test_config_flow.py index 21d74bb5e96..92c25e00927 100644 --- a/tests/components/geonetnz_volcano/test_config_flow.py +++ b/tests/components/geonetnz_volcano/test_config_flow.py @@ -1,5 +1,6 @@ """Define tests for the GeoNet NZ Volcano config flow.""" from datetime import timedelta +from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.components.geonetnz_volcano import config_flow @@ -11,8 +12,6 @@ from homeassistant.const import ( CONF_UNIT_SYSTEM, ) -from tests.async_mock import patch - async def test_duplicate_error(hass, config_entry): """Test that errors are shown when duplicates are added.""" diff --git a/tests/components/geonetnz_volcano/test_init.py b/tests/components/geonetnz_volcano/test_init.py index 4edf8f452fe..42915f7feaa 100644 --- a/tests/components/geonetnz_volcano/test_init.py +++ b/tests/components/geonetnz_volcano/test_init.py @@ -1,7 +1,7 @@ """Define tests for the GeoNet NZ Volcano general setup.""" -from homeassistant.components.geonetnz_volcano import DOMAIN, FEED +from unittest.mock import AsyncMock, patch -from tests.async_mock import AsyncMock, patch +from homeassistant.components.geonetnz_volcano import DOMAIN, FEED async def test_component_unload_config_entry(hass, config_entry): diff --git a/tests/components/geonetnz_volcano/test_sensor.py b/tests/components/geonetnz_volcano/test_sensor.py index 22157c241ac..824fc059ace 100644 --- a/tests/components/geonetnz_volcano/test_sensor.py +++ b/tests/components/geonetnz_volcano/test_sensor.py @@ -1,4 +1,6 @@ """The tests for the GeoNet NZ Volcano Feed integration.""" +from unittest.mock import AsyncMock, patch + from homeassistant.components import geonetnz_volcano from homeassistant.components.geo_location import ATTR_DISTANCE from homeassistant.components.geonetnz_volcano import DEFAULT_SCAN_INTERVAL @@ -21,7 +23,6 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM -from tests.async_mock import AsyncMock, patch from tests.common import async_fire_time_changed from tests.components.geonetnz_volcano import _generate_mock_feed_entry diff --git a/tests/components/gios/__init__.py b/tests/components/gios/__init__.py index 920461a6ae1..6b1aa982c71 100644 --- a/tests/components/gios/__init__.py +++ b/tests/components/gios/__init__.py @@ -1,9 +1,9 @@ """Tests for GIOS.""" import json +from unittest.mock import patch from homeassistant.components.gios.const import DOMAIN -from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture STATIONS = [ diff --git a/tests/components/gios/test_air_quality.py b/tests/components/gios/test_air_quality.py index 9a5b1be6a20..21a1abf637a 100644 --- a/tests/components/gios/test_air_quality.py +++ b/tests/components/gios/test_air_quality.py @@ -1,6 +1,7 @@ """Test air_quality of GIOS integration.""" from datetime import timedelta import json +from unittest.mock import patch from gios import ApiError @@ -24,7 +25,6 @@ from homeassistant.const import ( ) from homeassistant.util.dt import utcnow -from tests.async_mock import patch from tests.common import async_fire_time_changed, load_fixture from tests.components.gios import init_integration diff --git a/tests/components/gios/test_config_flow.py b/tests/components/gios/test_config_flow.py index 24ada20aded..830b3a198a5 100644 --- a/tests/components/gios/test_config_flow.py +++ b/tests/components/gios/test_config_flow.py @@ -1,5 +1,6 @@ """Define tests for the GIOS config flow.""" import json +from unittest.mock import patch from gios import ApiError @@ -8,7 +9,6 @@ from homeassistant.components.gios import config_flow from homeassistant.components.gios.const import CONF_STATION_ID from homeassistant.const import CONF_NAME -from tests.async_mock import patch from tests.common import load_fixture from tests.components.gios import STATIONS diff --git a/tests/components/gios/test_init.py b/tests/components/gios/test_init.py index 0846ddfb4ca..344afe4e047 100644 --- a/tests/components/gios/test_init.py +++ b/tests/components/gios/test_init.py @@ -1,4 +1,6 @@ """Test init of GIOS integration.""" +from unittest.mock import patch + from homeassistant.components.gios.const import DOMAIN from homeassistant.config_entries import ( ENTRY_STATE_LOADED, @@ -7,7 +9,6 @@ from homeassistant.config_entries import ( ) from homeassistant.const import STATE_UNAVAILABLE -from tests.async_mock import patch from tests.common import MockConfigEntry from tests.components.gios import init_integration diff --git a/tests/components/goalzero/__init__.py b/tests/components/goalzero/__init__.py index 0ba2a912766..fb531dfca4b 100644 --- a/tests/components/goalzero/__init__.py +++ b/tests/components/goalzero/__init__.py @@ -1,8 +1,8 @@ """Tests for the Goal Zero Yeti integration.""" -from homeassistant.const import CONF_HOST, CONF_NAME +from unittest.mock import AsyncMock, patch -from tests.async_mock import AsyncMock, patch +from homeassistant.const import CONF_HOST, CONF_NAME HOST = "1.2.3.4" NAME = "Yeti" diff --git a/tests/components/goalzero/test_config_flow.py b/tests/components/goalzero/test_config_flow.py index 906a84d7882..10ef02bfcff 100644 --- a/tests/components/goalzero/test_config_flow.py +++ b/tests/components/goalzero/test_config_flow.py @@ -1,4 +1,6 @@ """Test Goal Zero Yeti config flow.""" +from unittest.mock import patch + from goalzero import exceptions from homeassistant.components.goalzero.const import DOMAIN @@ -19,7 +21,6 @@ from . import ( _patch_config_flow_yeti, ) -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/gogogate2/test_config_flow.py b/tests/components/gogogate2/test_config_flow.py index 667c0330d80..2d19ee70609 100644 --- a/tests/components/gogogate2/test_config_flow.py +++ b/tests/components/gogogate2/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for the GogoGate2 component.""" +from unittest.mock import MagicMock, patch + from gogogate2_api import GogoGate2Api from gogogate2_api.common import ApiError from gogogate2_api.const import GogoGate2ApiErrorCode @@ -19,7 +21,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry MOCK_MAC_ADDR = "AA:BB:CC:DD:EE:FF" @@ -36,7 +37,9 @@ async def test_auth_fail( gogogate2api_mock.return_value = api api.reset_mock() - api.info.side_effect = ApiError(GogoGate2ApiErrorCode.CREDENTIALS_INCORRECT, "blah") + api.async_info.side_effect = ApiError( + GogoGate2ApiErrorCode.CREDENTIALS_INCORRECT, "blah" + ) result = await hass.config_entries.flow.async_init( "gogogate2", context={"source": SOURCE_USER} ) @@ -56,7 +59,7 @@ async def test_auth_fail( } api.reset_mock() - api.info.side_effect = Exception("Generic connection error.") + api.async_info.side_effect = Exception("Generic connection error.") result = await hass.config_entries.flow.async_init( "gogogate2", context={"source": SOURCE_USER} ) diff --git a/tests/components/gogogate2/test_cover.py b/tests/components/gogogate2/test_cover.py index 91bffca56ce..31810cb67d0 100644 --- a/tests/components/gogogate2/test_cover.py +++ b/tests/components/gogogate2/test_cover.py @@ -1,5 +1,6 @@ """Tests for the GogoGate2 component.""" from datetime import timedelta +from unittest.mock import MagicMock, patch from gogogate2_api import GogoGate2Api, ISmartGateApi from gogogate2_api.common import ( @@ -49,7 +50,6 @@ from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry, async_fire_time_changed, mock_device_registry @@ -183,7 +183,7 @@ def _mocked_ismartgate_closed_door_response(): async def test_import_fail(gogogate2api_mock, hass: HomeAssistant) -> None: """Test the failure to import.""" api = MagicMock(spec=GogoGate2Api) - api.info.side_effect = ApiError(22, "Error") + api.async_info.side_effect = ApiError(22, "Error") gogogate2api_mock.return_value = api hass_config = { @@ -216,11 +216,11 @@ async def test_import( ) -> None: """Test importing of file based config.""" api0 = MagicMock(spec=GogoGate2Api) - api0.info.return_value = _mocked_gogogate_open_door_response() + api0.async_info.return_value = _mocked_gogogate_open_door_response() gogogate2api_mock.return_value = api0 api1 = MagicMock(spec=ISmartGateApi) - api1.info.return_value = _mocked_ismartgate_closed_door_response() + api1.async_info.return_value = _mocked_ismartgate_closed_door_response() ismartgateapi_mock.return_value = api1 hass_config = { @@ -320,8 +320,8 @@ async def test_open_close_update(gogogate2api_mock, hass: HomeAssistant) -> None ) api = MagicMock(GogoGate2Api) - api.activate.return_value = GogoGate2ActivateResponse(result=True) - api.info.return_value = info_response(DoorStatus.OPENED) + api.async_activate.return_value = GogoGate2ActivateResponse(result=True) + api.async_info.return_value = info_response(DoorStatus.OPENED) gogogate2api_mock.return_value = api config_entry = MockConfigEntry( @@ -340,7 +340,7 @@ async def test_open_close_update(gogogate2api_mock, hass: HomeAssistant) -> None await hass.async_block_till_done() assert hass.states.get("cover.door1").state == STATE_OPEN - api.info.return_value = info_response(DoorStatus.CLOSED) + api.async_info.return_value = info_response(DoorStatus.CLOSED) await hass.services.async_call( COVER_DOMAIN, "close_cover", @@ -349,9 +349,9 @@ async def test_open_close_update(gogogate2api_mock, hass: HomeAssistant) -> None async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() assert hass.states.get("cover.door1").state == STATE_CLOSED - api.close_door.assert_called_with(1) + api.async_close_door.assert_called_with(1) - api.info.return_value = info_response(DoorStatus.OPENED) + api.async_info.return_value = info_response(DoorStatus.OPENED) await hass.services.async_call( COVER_DOMAIN, "open_cover", @@ -360,9 +360,9 @@ async def test_open_close_update(gogogate2api_mock, hass: HomeAssistant) -> None async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() assert hass.states.get("cover.door1").state == STATE_OPEN - api.open_door.assert_called_with(1) + api.async_open_door.assert_called_with(1) - api.info.return_value = info_response(DoorStatus.UNDEFINED) + api.async_info.return_value = info_response(DoorStatus.UNDEFINED) async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() assert hass.states.get("cover.door1").state == STATE_UNKNOWN @@ -377,7 +377,7 @@ async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None: closed_door_response = _mocked_ismartgate_closed_door_response() api = MagicMock(ISmartGateApi) - api.info.return_value = closed_door_response + api.async_info.return_value = closed_door_response ismartgateapi_mock.return_value = api config_entry = MockConfigEntry( @@ -405,14 +405,14 @@ async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None: == DEVICE_CLASS_GATE ) - api.info.side_effect = Exception("Error") + api.async_info.side_effect = Exception("Error") async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() assert hass.states.get("cover.door1").state == STATE_UNAVAILABLE - api.info.side_effect = None - api.info.return_value = closed_door_response + api.async_info.side_effect = None + api.async_info.return_value = closed_door_response async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() assert hass.states.get("cover.door1").state == STATE_CLOSED @@ -426,7 +426,7 @@ async def test_device_info_ismartgate(ismartgateapi_mock, hass: HomeAssistant) - closed_door_response = _mocked_ismartgate_closed_door_response() api = MagicMock(ISmartGateApi) - api.info.return_value = closed_door_response + api.async_info.return_value = closed_door_response ismartgateapi_mock.return_value = api config_entry = MockConfigEntry( @@ -445,7 +445,7 @@ async def test_device_info_ismartgate(ismartgateapi_mock, hass: HomeAssistant) - assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - device = device_registry.async_get_device({(DOMAIN, "xyz")}, set()) + device = device_registry.async_get_device({(DOMAIN, "xyz")}) assert device assert device.manufacturer == MANUFACTURER assert device.name == "mycontroller" @@ -461,7 +461,7 @@ async def test_device_info_gogogate2(gogogate2api_mock, hass: HomeAssistant) -> closed_door_response = _mocked_gogogate_open_door_response() api = MagicMock(GogoGate2Api) - api.info.return_value = closed_door_response + api.async_info.return_value = closed_door_response gogogate2api_mock.return_value = api config_entry = MockConfigEntry( @@ -480,7 +480,7 @@ async def test_device_info_gogogate2(gogogate2api_mock, hass: HomeAssistant) -> assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - device = device_registry.async_get_device({(DOMAIN, "xyz")}, set()) + device = device_registry.async_get_device({(DOMAIN, "xyz")}) assert device assert device.manufacturer == MANUFACTURER assert device.name == "mycontroller" diff --git a/tests/components/gogogate2/test_init.py b/tests/components/gogogate2/test_init.py index af8678300d1..f51f08b2319 100644 --- a/tests/components/gogogate2/test_init.py +++ b/tests/components/gogogate2/test_init.py @@ -1,4 +1,6 @@ """Tests for the GogoGate2 component.""" +from unittest.mock import MagicMock, patch + from gogogate2_api import GogoGate2Api import pytest @@ -15,7 +17,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry @@ -24,7 +25,7 @@ async def test_config_update(gogogate2api_mock, hass: HomeAssistant) -> None: """Test config setup where the config is updated.""" api = MagicMock(GogoGate2Api) - api.info.side_effect = Exception("Error") + api.async_info.side_effect = Exception("Error") gogogate2api_mock.return_value = api config_entry = MockConfigEntry( @@ -52,7 +53,7 @@ async def test_config_update(gogogate2api_mock, hass: HomeAssistant) -> None: async def test_config_no_update(ismartgateapi_mock, hass: HomeAssistant) -> None: """Test config setup where the data is not updated.""" api = MagicMock(GogoGate2Api) - api.info.side_effect = Exception("Error") + api.async_info.side_effect = Exception("Error") ismartgateapi_mock.return_value = api config_entry = MockConfigEntry( diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 6ec75ad53f6..20cb13130ec 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -1,7 +1,7 @@ """Test configuration and mocks for the google integration.""" -import pytest +from unittest.mock import patch -from tests.async_mock import patch +import pytest TEST_CALENDAR = { "id": "qwertyuiopasdfghjklzxcvbnm@import.calendar.google.com", diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index 92f03396965..ad7b6b12001 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -1,5 +1,6 @@ """The tests for the google calendar platform.""" import copy +from unittest.mock import Mock, patch import httplib2 import pytest @@ -22,7 +23,6 @@ from homeassistant.setup import async_setup_component from homeassistant.util import slugify import homeassistant.util.dt as dt_util -from tests.async_mock import Mock, patch from tests.common import async_mock_service GOOGLE_CONFIG = {CONF_CLIENT_ID: "client_id", CONF_CLIENT_SECRET: "client_secret"} diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index e3412a01f5e..d90efa29f6c 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -1,12 +1,12 @@ """The tests for the Google Calendar component.""" +from unittest.mock import patch + import pytest import homeassistant.components.google as google from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.setup import async_setup_component -from tests.async_mock import patch - @pytest.fixture(name="google_setup") def mock_google_setup(hass): diff --git a/tests/components/google_assistant/__init__.py b/tests/components/google_assistant/__init__.py index bbc5b92615c..cb11f1ceaac 100644 --- a/tests/components/google_assistant/__init__.py +++ b/tests/components/google_assistant/__init__.py @@ -1,7 +1,7 @@ """Tests for the Google Assistant integration.""" -from homeassistant.components.google_assistant import helpers +from unittest.mock import MagicMock -from tests.async_mock import MagicMock +from homeassistant.components.google_assistant import helpers def mock_google_config_store(agent_user_ids=None): @@ -143,6 +143,15 @@ DEMO_DEVICES = [ "type": "action.devices.types.BLINDS", "willReportState": False, }, + { + "id": "cover.pergola_roof", + "name": {"name": "Pergola Roof"}, + "traits": [ + "action.devices.traits.OpenClose", + ], + "type": "action.devices.types.BLINDS", + "willReportState": False, + }, { "id": "cover.hall_window", "name": {"name": "Hall Window"}, diff --git a/tests/components/google_assistant/test_helpers.py b/tests/components/google_assistant/test_helpers.py index 87e3cac657c..abf2773d67e 100644 --- a/tests/components/google_assistant/test_helpers.py +++ b/tests/components/google_assistant/test_helpers.py @@ -1,5 +1,6 @@ """Test Google Assistant helpers.""" from datetime import timedelta +from unittest.mock import Mock, call, patch import pytest @@ -15,7 +16,6 @@ from homeassistant.util import dt from . import MockConfig -from tests.async_mock import Mock, call, patch from tests.common import ( async_capture_events, async_fire_time_changed, diff --git a/tests/components/google_assistant/test_http.py b/tests/components/google_assistant/test_http.py index 4b9461e6304..69a8242b7cc 100644 --- a/tests/components/google_assistant/test_http.py +++ b/tests/components/google_assistant/test_http.py @@ -1,5 +1,6 @@ """Test Google http services.""" from datetime import datetime, timedelta, timezone +from unittest.mock import ANY, patch from homeassistant.components.google_assistant import GOOGLE_ASSISTANT_SCHEMA from homeassistant.components.google_assistant.const import ( @@ -12,8 +13,6 @@ from homeassistant.components.google_assistant.http import ( _get_homegraph_token, ) -from tests.async_mock import ANY, patch - DUMMY_CONFIG = GOOGLE_ASSISTANT_SCHEMA( { "project_id": "1234", diff --git a/tests/components/google_assistant/test_report_state.py b/tests/components/google_assistant/test_report_state.py index cd967e6c82e..72130dbfdb9 100644 --- a/tests/components/google_assistant/test_report_state.py +++ b/tests/components/google_assistant/test_report_state.py @@ -1,15 +1,18 @@ """Test Google report state.""" +from unittest.mock import AsyncMock, patch + from homeassistant.components.google_assistant import error, report_state +from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow from . import BASIC_CONFIG -from tests.async_mock import AsyncMock, patch from tests.common import async_fire_time_changed async def test_report_state(hass, caplog, legacy_patchable_time): """Test report state works.""" + assert await async_setup_component(hass, "switch", {}) hass.states.async_set("light.ceiling", "off") hass.states.async_set("switch.ac", "on") @@ -43,14 +46,11 @@ async def test_report_state(hass, caplog, legacy_patchable_time): "devices": {"states": {"light.kitchen": {"on": True, "online": True}}} } - # Test that state changes that change something that Google doesn't care about - # do not trigger a state report. + # Test that only significant state changes are reported with patch.object( BASIC_CONFIG, "async_report_state_all", AsyncMock() ) as mock_report: - hass.states.async_set( - "light.kitchen", "on", {"irrelevant": "should_be_ignored"} - ) + hass.states.async_set("switch.ac", "on", {"something": "else"}) await hass.async_block_till_done() assert len(mock_report.mock_calls) == 0 diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index ebe34c2aa95..9c8f9a48338 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -1,4 +1,6 @@ """Test Google Smart Home.""" +from unittest.mock import patch + import pytest from homeassistant.components import camera @@ -28,7 +30,6 @@ from homeassistant.setup import async_setup_component from . import BASIC_CONFIG, MockConfig -from tests.async_mock import patch from tests.common import mock_area_registry, mock_device_registry, mock_registry REQ_ID = "ff36a3cc-ec34-11e6-b1a0-64510650abcf" @@ -159,6 +160,9 @@ async def test_sync_in_area(area_on_device, hass, registries): device = registries.device.async_get_or_create( config_entry_id="1234", + manufacturer="Someone", + model="Some model", + sw_version="Some Version", connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) registries.device.async_update_device( @@ -248,6 +252,11 @@ async def test_sync_in_area(area_on_device, hass, registries): "temperatureMaxK": 6535, }, }, + "deviceInfo": { + "manufacturer": "Someone", + "model": "Some model", + "swVersion": "Some Version", + }, "roomHint": "Living Room", } ], diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 4946416e1c8..9b573f1cf71 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1,5 +1,6 @@ """Tests for the Google Assistant traits.""" from datetime import datetime, timedelta +from unittest.mock import patch import pytest @@ -53,7 +54,6 @@ from homeassistant.util import color from . import BASIC_CONFIG, MockConfig -from tests.async_mock import patch from tests.common import async_mock_service REQ_ID = "ff36a3cc-ec34-11e6-b1a0-64510650abcf" @@ -384,8 +384,8 @@ async def test_startstop_vacuum(hass): assert unpause_calls[0].data == {ATTR_ENTITY_ID: "vacuum.bla"} -async def test_startstop_covert(hass): - """Test startStop trait support for vacuum domain.""" +async def test_startstop_cover(hass): + """Test startStop trait support for cover domain.""" assert helpers.get_google_type(cover.DOMAIN, None) is not None assert trait.StartStopTrait.supported(cover.DOMAIN, cover.SUPPORT_STOP, None) @@ -429,6 +429,24 @@ async def test_startstop_covert(hass): await trt.execute(trait.COMMAND_PAUSEUNPAUSE, BASIC_DATA, {"start": True}, {}) +async def test_startstop_cover_assumed(hass): + """Test startStop trait support for cover domain of assumed state.""" + trt = trait.StartStopTrait( + hass, + State( + "cover.bla", + cover.STATE_CLOSED, + {ATTR_SUPPORTED_FEATURES: cover.SUPPORT_STOP, ATTR_ASSUMED_STATE: True}, + ), + BASIC_CONFIG, + ) + + stop_calls = async_mock_service(hass, cover.DOMAIN, cover.SERVICE_STOP_COVER) + await trt.execute(trait.COMMAND_STARTSTOP, BASIC_DATA, {"start": False}, {}) + assert len(stop_calls) == 1 + assert stop_calls[0].data == {ATTR_ENTITY_ID: "cover.bla"} + + async def test_color_setting_color_light(hass): """Test ColorSpectrum trait support for light domain.""" assert helpers.get_google_type(light.DOMAIN, None) is not None diff --git a/tests/components/google_pubsub/test_init.py b/tests/components/google_pubsub/test_init.py index 96be5b3ed62..c174e454701 100644 --- a/tests/components/google_pubsub/test_init.py +++ b/tests/components/google_pubsub/test_init.py @@ -1,6 +1,7 @@ """The tests for the Google Pub/Sub component.""" from dataclasses import dataclass from datetime import datetime +import unittest.mock as mock import pytest @@ -10,8 +11,6 @@ from homeassistant.const import EVENT_STATE_CHANGED from homeassistant.core import split_entity_id from homeassistant.setup import async_setup_component -import tests.async_mock as mock - GOOGLE_PUBSUB_PATH = "homeassistant.components.google_pubsub" diff --git a/tests/components/google_translate/test_tts.py b/tests/components/google_translate/test_tts.py index 79c303fd2ff..5690591ccd2 100644 --- a/tests/components/google_translate/test_tts.py +++ b/tests/components/google_translate/test_tts.py @@ -1,6 +1,7 @@ """The tests for the Google speech platform.""" import os import shutil +from unittest.mock import patch from gtts import gTTSError import pytest @@ -14,7 +15,6 @@ import homeassistant.components.tts as tts from homeassistant.config import async_process_ha_core_config from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import async_mock_service from tests.components.tts.test_init import mutagen_mock # noqa: F401 diff --git a/tests/components/google_wifi/test_sensor.py b/tests/components/google_wifi/test_sensor.py index a9e09c8b66d..06ad5e0c3ea 100644 --- a/tests/components/google_wifi/test_sensor.py +++ b/tests/components/google_wifi/test_sensor.py @@ -1,12 +1,12 @@ """The tests for the Google Wifi platform.""" from datetime import datetime, timedelta +from unittest.mock import Mock, patch import homeassistant.components.google_wifi.sensor as google_wifi from homeassistant.const import STATE_UNKNOWN from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from tests.async_mock import Mock, patch from tests.common import assert_setup_component, async_fire_time_changed NAME = "foo" diff --git a/tests/components/gpslogger/test_init.py b/tests/components/gpslogger/test_init.py index 39dc05303b3..d30f57f0f33 100644 --- a/tests/components/gpslogger/test_init.py +++ b/tests/components/gpslogger/test_init.py @@ -1,4 +1,6 @@ """The tests the for GPSLogger device tracker platform.""" +from unittest.mock import patch + import pytest from homeassistant import data_entry_flow @@ -15,8 +17,6 @@ from homeassistant.const import ( from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component -from tests.async_mock import patch - HOME_LATITUDE = 37.239622 HOME_LONGITUDE = -115.815811 diff --git a/tests/components/graphite/test_init.py b/tests/components/graphite/test_init.py index 19b8c165f37..88be3723936 100644 --- a/tests/components/graphite/test_init.py +++ b/tests/components/graphite/test_init.py @@ -2,6 +2,7 @@ import socket import unittest from unittest import mock +from unittest.mock import patch import homeassistant.components.graphite as graphite from homeassistant.const import ( @@ -14,7 +15,6 @@ from homeassistant.const import ( import homeassistant.core as ha from homeassistant.setup import setup_component -from tests.async_mock import patch from tests.common import get_test_home_assistant diff --git a/tests/components/gree/common.py b/tests/components/gree/common.py index 8894730c8ca..d9fcfba39ce 100644 --- a/tests/components/gree/common.py +++ b/tests/components/gree/common.py @@ -1,5 +1,5 @@ """Common helpers for gree test cases.""" -from tests.async_mock import AsyncMock, Mock +from unittest.mock import AsyncMock, Mock def build_device_info_mock( diff --git a/tests/components/gree/conftest.py b/tests/components/gree/conftest.py index b102f0f36fe..bc9a6451dce 100644 --- a/tests/components/gree/conftest.py +++ b/tests/components/gree/conftest.py @@ -1,10 +1,10 @@ """Pytest module configuration.""" +from unittest.mock import AsyncMock, patch + import pytest from .common import build_device_info_mock, build_device_mock -from tests.async_mock import AsyncMock, patch - @pytest.fixture(name="discovery") def discovery_fixture(): diff --git a/tests/components/gree/test_climate.py b/tests/components/gree/test_climate.py index 534168fa78e..d85976c2410 100644 --- a/tests/components/gree/test_climate.py +++ b/tests/components/gree/test_climate.py @@ -1,5 +1,6 @@ """Tests for gree component.""" from datetime import timedelta +from unittest.mock import DEFAULT as DEFAULT_MOCK, AsyncMock, patch from greeclimate.device import HorizontalSwing, VerticalSwing from greeclimate.exceptions import DeviceNotBoundError, DeviceTimeoutError @@ -51,6 +52,8 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_SUPPORTED_FEATURES, ATTR_TEMPERATURE, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, STATE_UNAVAILABLE, ) from homeassistant.setup import async_setup_component @@ -58,7 +61,6 @@ import homeassistant.util.dt as dt_util from .common import build_device_mock -from tests.async_mock import DEFAULT as DEFAULT_MOCK, AsyncMock, patch from tests.common import MockConfigEntry, async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake_device_1" @@ -244,14 +246,14 @@ async def test_send_power_on(hass, discovery, device, mock_now): assert await hass.services.async_call( DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVAC_MODE_AUTO}, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ID}, blocking=True, ) state = hass.states.get(ENTITY_ID) assert state is not None - assert state.state == HVAC_MODE_AUTO + assert state.state != HVAC_MODE_OFF async def test_send_power_on_device_timeout(hass, discovery, device, mock_now): @@ -262,14 +264,58 @@ async def test_send_power_on_device_timeout(hass, discovery, device, mock_now): assert await hass.services.async_call( DOMAIN, - SERVICE_SET_HVAC_MODE, - {ATTR_ENTITY_ID: ENTITY_ID, ATTR_HVAC_MODE: HVAC_MODE_AUTO}, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_ID}, blocking=True, ) state = hass.states.get(ENTITY_ID) assert state is not None - assert state.state == HVAC_MODE_AUTO + assert state.state != HVAC_MODE_OFF + + +async def test_send_power_off(hass, discovery, device, mock_now): + """Test for sending power off command to the device.""" + await async_setup_gree(hass) + + next_update = mock_now + timedelta(minutes=5) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + assert await hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(ENTITY_ID) + assert state is not None + assert state.state == HVAC_MODE_OFF + + +async def test_send_power_off_device_timeout(hass, discovery, device, mock_now): + """Test for sending power off command to the device with a device timeout.""" + device().push_state_update.side_effect = DeviceTimeoutError + + await async_setup_gree(hass) + + next_update = mock_now + timedelta(minutes=5) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + assert await hass.services.async_call( + DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_ID}, + blocking=True, + ) + + state = hass.states.get(ENTITY_ID) + assert state is not None + assert state.state == HVAC_MODE_OFF async def test_send_target_temperature(hass, discovery, device, mock_now): diff --git a/tests/components/gree/test_init.py b/tests/components/gree/test_init.py index ef693c9538a..bf999ee9e6f 100644 --- a/tests/components/gree/test_init.py +++ b/tests/components/gree/test_init.py @@ -1,10 +1,11 @@ """Tests for the Gree Integration.""" +from unittest.mock import patch + from homeassistant.components.gree.const import DOMAIN as GREE_DOMAIN from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/griddy/test_config_flow.py b/tests/components/griddy/test_config_flow.py index 0b2656dcf09..cfc2b23a8ed 100644 --- a/tests/components/griddy/test_config_flow.py +++ b/tests/components/griddy/test_config_flow.py @@ -1,11 +1,10 @@ """Test the Griddy Power config flow.""" import asyncio +from unittest.mock import MagicMock, patch from homeassistant import config_entries, setup from homeassistant.components.griddy.const import DOMAIN -from tests.async_mock import MagicMock, patch - async def test_form(hass): """Test we get the form.""" diff --git a/tests/components/griddy/test_sensor.py b/tests/components/griddy/test_sensor.py index ae3d0c3be84..46f8d238c49 100644 --- a/tests/components/griddy/test_sensor.py +++ b/tests/components/griddy/test_sensor.py @@ -1,13 +1,13 @@ """The sensor tests for the griddy platform.""" import json import os +from unittest.mock import patch from griddypower.async_api import GriddyPriceData from homeassistant.components.griddy import CONF_LOADZONE, DOMAIN from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import load_fixture diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index 77f137c6ee1..627e3c5bbe0 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -1,6 +1,7 @@ """The tests for the Group components.""" # pylint: disable=protected-access from collections import OrderedDict +from unittest.mock import patch import homeassistant.components.group as group from homeassistant.const import ( @@ -19,7 +20,6 @@ from homeassistant.core import CoreState from homeassistant.helpers.event import TRACK_STATE_CHANGE_CALLBACKS from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import assert_setup_component from tests.components.group import common diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 8ce9f4bf9f3..136da458f66 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -1,5 +1,7 @@ """The tests for the Group Light platform.""" from os import path +import unittest.mock +from unittest.mock import MagicMock, patch from homeassistant import config as hass_config from homeassistant.components.group import DOMAIN, SERVICE_RELOAD @@ -31,9 +33,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -import tests.async_mock -from tests.async_mock import MagicMock, patch - async def test_default_state(hass): """Test light group default state.""" @@ -603,7 +602,7 @@ async def test_invalid_service_calls(hass): grouped_light = add_entities.call_args[0][0][0] grouped_light.hass = hass - with tests.async_mock.patch.object(hass.services, "async_call") as mock_call: + with unittest.mock.patch.object(hass.services, "async_call") as mock_call: await grouped_light.async_turn_on(brightness=150, four_oh_four="404") data = {ATTR_ENTITY_ID: ["light.test1", "light.test2"], ATTR_BRIGHTNESS: 150} mock_call.assert_called_once_with( diff --git a/tests/components/group/test_notify.py b/tests/components/group/test_notify.py index 05a23fa4e7a..a0f210c68a2 100644 --- a/tests/components/group/test_notify.py +++ b/tests/components/group/test_notify.py @@ -1,5 +1,6 @@ """The tests for the notify.group platform.""" from os import path +from unittest.mock import MagicMock, patch from homeassistant import config as hass_config import homeassistant.components.demo.notify as demo @@ -8,8 +9,6 @@ import homeassistant.components.group.notify as group import homeassistant.components.notify as notify from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, patch - async def test_send_message_with_data(hass): """Test sending a message with to a notify group.""" diff --git a/tests/components/group/test_reproduce_state.py b/tests/components/group/test_reproduce_state.py index 13422cd0826..58bbc94876e 100644 --- a/tests/components/group/test_reproduce_state.py +++ b/tests/components/group/test_reproduce_state.py @@ -1,12 +1,11 @@ """The tests for reproduction of state.""" from asyncio import Future +from unittest.mock import patch from homeassistant.components.group.reproduce_state import async_reproduce_states from homeassistant.core import Context, State -from tests.async_mock import patch - async def test_reproduce_group(hass): """Test reproduce_state with group.""" diff --git a/tests/components/guardian/conftest.py b/tests/components/guardian/conftest.py index cfa9174ef57..1a83222c575 100644 --- a/tests/components/guardian/conftest.py +++ b/tests/components/guardian/conftest.py @@ -1,7 +1,7 @@ """Define fixtures for Elexa Guardian tests.""" -import pytest +from unittest.mock import patch -from tests.async_mock import patch +import pytest @pytest.fixture() diff --git a/tests/components/guardian/test_config_flow.py b/tests/components/guardian/test_config_flow.py index db0cf877d37..e9b53b4e629 100644 --- a/tests/components/guardian/test_config_flow.py +++ b/tests/components/guardian/test_config_flow.py @@ -1,4 +1,6 @@ """Define tests for the Elexa Guardian config flow.""" +from unittest.mock import patch + from aioguardian.errors import GuardianError from homeassistant import data_entry_flow @@ -10,7 +12,6 @@ from homeassistant.components.guardian.config_flow import ( from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/hangouts/test_config_flow.py b/tests/components/hangouts/test_config_flow.py index 9cdb5799951..93f909d3bd4 100644 --- a/tests/components/hangouts/test_config_flow.py +++ b/tests/components/hangouts/test_config_flow.py @@ -1,11 +1,11 @@ """Tests for the Google Hangouts config flow.""" +from unittest.mock import patch + from homeassistant import data_entry_flow from homeassistant.components.hangouts import config_flow from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from tests.async_mock import patch - EMAIL = "test@test.com" PASSWORD = "1232456" diff --git a/tests/components/harmony/conftest.py b/tests/components/harmony/conftest.py new file mode 100644 index 00000000000..e758a2795a9 --- /dev/null +++ b/tests/components/harmony/conftest.py @@ -0,0 +1,147 @@ +"""Fixtures for harmony tests.""" +import logging +from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch + +from aioharmony.const import ClientCallbackType +import pytest + +from homeassistant.components.harmony.const import ACTIVITY_POWER_OFF + +_LOGGER = logging.getLogger(__name__) + +WATCH_TV_ACTIVITY_ID = 123 +PLAY_MUSIC_ACTIVITY_ID = 456 + +ACTIVITIES_TO_IDS = { + ACTIVITY_POWER_OFF: -1, + "Watch TV": WATCH_TV_ACTIVITY_ID, + "Play Music": PLAY_MUSIC_ACTIVITY_ID, +} + +IDS_TO_ACTIVITIES = { + -1: ACTIVITY_POWER_OFF, + WATCH_TV_ACTIVITY_ID: "Watch TV", + PLAY_MUSIC_ACTIVITY_ID: "Play Music", +} + +TV_DEVICE_ID = 1234 +TV_DEVICE_NAME = "My TV" + +DEVICES_TO_IDS = { + TV_DEVICE_NAME: TV_DEVICE_ID, +} + +IDS_TO_DEVICES = { + TV_DEVICE_ID: TV_DEVICE_NAME, +} + + +class FakeHarmonyClient: + """FakeHarmonyClient to mock away network calls.""" + + def __init__( + self, ip_address: str = "", callbacks: ClientCallbackType = MagicMock() + ): + """Initialize FakeHarmonyClient class.""" + self._activity_name = "Watch TV" + self.close = AsyncMock() + self.send_commands = AsyncMock() + self.change_channel = AsyncMock() + self.sync = AsyncMock() + self._callbacks = callbacks + + async def connect(self): + """Connect and call the appropriate callbacks.""" + self._callbacks.connect(None) + return AsyncMock(return_value=(True)) + + def get_activity_name(self, activity_id): + """Return the activity name with the given activity_id.""" + return IDS_TO_ACTIVITIES.get(activity_id) + + def get_activity_id(self, activity_name): + """Return the mapping of an activity name to the internal id.""" + return ACTIVITIES_TO_IDS.get(activity_name) + + def get_device_name(self, device_id): + """Return the device name with the given device_id.""" + return IDS_TO_DEVICES.get(device_id) + + def get_device_id(self, device_name): + """Return the device id with the given device_name.""" + return DEVICES_TO_IDS.get(device_name) + + async def start_activity(self, activity_id): + """Update the current activity and call the appropriate callbacks.""" + self._activity_name = IDS_TO_ACTIVITIES.get(int(activity_id)) + activity_tuple = (activity_id, self._activity_name) + self._callbacks.new_activity_starting(activity_tuple) + self._callbacks.new_activity(activity_tuple) + + return AsyncMock(return_value=(True, "unused message")) + + async def power_off(self): + """Power off all activities.""" + await self.start_activity(-1) + + @property + def current_activity(self): + """Return the current activity tuple.""" + return ( + self.get_activity_id(self._activity_name), + self._activity_name, + ) + + @property + def config(self): + """Return the config object.""" + return self.hub_config.config + + @property + def json_config(self): + """Return the json config as a dict.""" + return {} + + @property + def hub_config(self): + """Return the client_config type.""" + config = MagicMock() + type(config).activities = PropertyMock( + return_value=[ + {"name": "Watch TV", "id": WATCH_TV_ACTIVITY_ID}, + {"name": "Play Music", "id": PLAY_MUSIC_ACTIVITY_ID}, + ] + ) + type(config).devices = PropertyMock( + return_value=[{"name": TV_DEVICE_NAME, "id": TV_DEVICE_ID}] + ) + type(config).info = PropertyMock(return_value={}) + type(config).hub_state = PropertyMock(return_value={}) + type(config).config = PropertyMock( + return_value={ + "activity": [ + {"id": WATCH_TV_ACTIVITY_ID, "label": "Watch TV"}, + {"id": PLAY_MUSIC_ACTIVITY_ID, "label": "Play Music"}, + ] + } + ) + return config + + +@pytest.fixture() +def mock_hc(): + """Create a mock HarmonyClient.""" + with patch( + "homeassistant.components.harmony.data.HarmonyClient", + side_effect=FakeHarmonyClient, + ) as fake: + yield fake + + +@pytest.fixture() +def mock_write_config(): + """Patches write_config_file to remove side effects.""" + with patch( + "homeassistant.components.harmony.remote.HarmonyRemote.write_config_file", + ) as mock: + yield mock diff --git a/tests/components/harmony/const.py b/tests/components/harmony/const.py new file mode 100644 index 00000000000..1911ea949af --- /dev/null +++ b/tests/components/harmony/const.py @@ -0,0 +1,6 @@ +"""Constants for Logitch Harmony Hub tests.""" + +HUB_NAME = "Guest Room" +ENTITY_REMOTE = "remote.guest_room" +ENTITY_WATCH_TV = "switch.guest_room_watch_tv" +ENTITY_PLAY_MUSIC = "switch.guest_room_play_music" diff --git a/tests/components/harmony/test_activity_changes.py b/tests/components/harmony/test_activity_changes.py new file mode 100644 index 00000000000..ff76c3ce998 --- /dev/null +++ b/tests/components/harmony/test_activity_changes.py @@ -0,0 +1,137 @@ +"""Test the Logitech Harmony Hub activity switches.""" + +import logging + +from homeassistant.components.harmony.const import DOMAIN +from homeassistant.components.remote import ATTR_ACTIVITY, DOMAIN as REMOTE_DOMAIN +from homeassistant.components.switch import ( + DOMAIN as SWITCH_DOMAIN, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_HOST, + CONF_NAME, + STATE_OFF, + STATE_ON, +) + +from .conftest import ACTIVITIES_TO_IDS +from .const import ENTITY_PLAY_MUSIC, ENTITY_REMOTE, ENTITY_WATCH_TV, HUB_NAME + +from tests.common import MockConfigEntry + +_LOGGER = logging.getLogger(__name__) + + +async def test_switch_toggles(mock_hc, hass, mock_write_config): + """Ensure calls to the switch modify the harmony state.""" + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} + ) + + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # mocks start with current activity == Watch TV + assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) + + # turn off watch tv switch + await _toggle_switch_and_wait(hass, SERVICE_TURN_OFF, ENTITY_WATCH_TV) + assert hass.states.is_state(ENTITY_REMOTE, STATE_OFF) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_OFF) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) + + # turn on play music switch + await _toggle_switch_and_wait(hass, SERVICE_TURN_ON, ENTITY_PLAY_MUSIC) + assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_OFF) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_ON) + + # turn on watch tv switch + await _toggle_switch_and_wait(hass, SERVICE_TURN_ON, ENTITY_WATCH_TV) + assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) + + +async def test_remote_toggles(mock_hc, hass, mock_write_config): + """Ensure calls to the remote also updates the switches.""" + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} + ) + + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + # mocks start with current activity == Watch TV + assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) + + # turn off remote + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: ENTITY_REMOTE}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.is_state(ENTITY_REMOTE, STATE_OFF) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_OFF) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) + + # turn on remote, restoring the last activity + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_REMOTE}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) + + # send new activity command, with activity name + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_REMOTE, ATTR_ACTIVITY: "Play Music"}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_OFF) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_ON) + + # send new activity command, with activity id + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_REMOTE, ATTR_ACTIVITY: ACTIVITIES_TO_IDS["Watch TV"]}, + blocking=True, + ) + await hass.async_block_till_done() + + assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) + + +async def _toggle_switch_and_wait(hass, service_name, entity): + await hass.services.async_call( + SWITCH_DOMAIN, + service_name, + {ATTR_ENTITY_ID: entity}, + blocking=True, + ) + await hass.async_block_till_done() diff --git a/tests/components/harmony/test_commands.py b/tests/components/harmony/test_commands.py new file mode 100644 index 00000000000..62056a08e1d --- /dev/null +++ b/tests/components/harmony/test_commands.py @@ -0,0 +1,263 @@ +"""Test sending commands to the Harmony Hub remote.""" + +from aioharmony.const import SendCommandDevice + +from homeassistant.components.harmony.const import ( + DOMAIN, + SERVICE_CHANGE_CHANNEL, + SERVICE_SYNC, +) +from homeassistant.components.harmony.remote import ATTR_CHANNEL, ATTR_DELAY_SECS +from homeassistant.components.remote import ( + ATTR_COMMAND, + ATTR_DEVICE, + ATTR_NUM_REPEATS, + DEFAULT_DELAY_SECS, + DEFAULT_HOLD_SECS, + DOMAIN as REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, +) +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME + +from .conftest import TV_DEVICE_ID, TV_DEVICE_NAME +from .const import ENTITY_REMOTE, HUB_NAME + +from tests.common import MockConfigEntry + +PLAY_COMMAND = "Play" +STOP_COMMAND = "Stop" + + +async def test_async_send_command(mock_hc, hass, mock_write_config): + """Ensure calls to send remote commands properly propagate to devices.""" + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} + ) + + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + data = hass.data[DOMAIN][entry.entry_id] + send_commands_mock = data._client.send_commands + + # No device provided + await _send_commands_and_wait( + hass, {ATTR_ENTITY_ID: ENTITY_REMOTE, ATTR_COMMAND: PLAY_COMMAND} + ) + send_commands_mock.assert_not_awaited() + + # Tell the TV to play by id + await _send_commands_and_wait( + hass, + { + ATTR_ENTITY_ID: ENTITY_REMOTE, + ATTR_COMMAND: PLAY_COMMAND, + ATTR_DEVICE: TV_DEVICE_ID, + }, + ) + + send_commands_mock.assert_awaited_once_with( + [ + SendCommandDevice( + device=str(TV_DEVICE_ID), + command=PLAY_COMMAND, + delay=float(DEFAULT_HOLD_SECS), + ), + DEFAULT_DELAY_SECS, + ] + ) + send_commands_mock.reset_mock() + + # Tell the TV to play by name + await _send_commands_and_wait( + hass, + { + ATTR_ENTITY_ID: ENTITY_REMOTE, + ATTR_COMMAND: PLAY_COMMAND, + ATTR_DEVICE: TV_DEVICE_NAME, + }, + ) + + send_commands_mock.assert_awaited_once_with( + [ + SendCommandDevice( + device=TV_DEVICE_ID, + command=PLAY_COMMAND, + delay=float(DEFAULT_HOLD_SECS), + ), + DEFAULT_DELAY_SECS, + ] + ) + send_commands_mock.reset_mock() + + # Tell the TV to play and stop by name + await _send_commands_and_wait( + hass, + { + ATTR_ENTITY_ID: ENTITY_REMOTE, + ATTR_COMMAND: [PLAY_COMMAND, STOP_COMMAND], + ATTR_DEVICE: TV_DEVICE_NAME, + }, + ) + + send_commands_mock.assert_awaited_once_with( + [ + SendCommandDevice( + device=TV_DEVICE_ID, + command=PLAY_COMMAND, + delay=float(DEFAULT_HOLD_SECS), + ), + DEFAULT_DELAY_SECS, + SendCommandDevice( + device=TV_DEVICE_ID, + command=STOP_COMMAND, + delay=float(DEFAULT_HOLD_SECS), + ), + DEFAULT_DELAY_SECS, + ] + ) + send_commands_mock.reset_mock() + + # Tell the TV to play by name multiple times + await _send_commands_and_wait( + hass, + { + ATTR_ENTITY_ID: ENTITY_REMOTE, + ATTR_COMMAND: PLAY_COMMAND, + ATTR_DEVICE: TV_DEVICE_NAME, + ATTR_NUM_REPEATS: 2, + }, + ) + + send_commands_mock.assert_awaited_once_with( + [ + SendCommandDevice( + device=TV_DEVICE_ID, + command=PLAY_COMMAND, + delay=float(DEFAULT_HOLD_SECS), + ), + DEFAULT_DELAY_SECS, + SendCommandDevice( + device=TV_DEVICE_ID, + command=PLAY_COMMAND, + delay=float(DEFAULT_HOLD_SECS), + ), + DEFAULT_DELAY_SECS, + ] + ) + send_commands_mock.reset_mock() + + # Send commands to an unknown device + await _send_commands_and_wait( + hass, + { + ATTR_ENTITY_ID: ENTITY_REMOTE, + ATTR_COMMAND: PLAY_COMMAND, + ATTR_DEVICE: "no-such-device", + }, + ) + send_commands_mock.assert_not_awaited() + send_commands_mock.reset_mock() + + +async def test_async_send_command_custom_delay(mock_hc, hass, mock_write_config): + """Ensure calls to send remote commands properly propagate to devices with custom delays.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "192.0.2.0", + CONF_NAME: HUB_NAME, + ATTR_DELAY_SECS: DEFAULT_DELAY_SECS + 2, + }, + ) + + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + data = hass.data[DOMAIN][entry.entry_id] + send_commands_mock = data._client.send_commands + + # Tell the TV to play by id + await _send_commands_and_wait( + hass, + { + ATTR_ENTITY_ID: ENTITY_REMOTE, + ATTR_COMMAND: PLAY_COMMAND, + ATTR_DEVICE: TV_DEVICE_ID, + }, + ) + + send_commands_mock.assert_awaited_once_with( + [ + SendCommandDevice( + device=str(TV_DEVICE_ID), + command=PLAY_COMMAND, + delay=float(DEFAULT_HOLD_SECS), + ), + DEFAULT_DELAY_SECS + 2, + ] + ) + send_commands_mock.reset_mock() + + +async def test_change_channel(mock_hc, hass, mock_write_config): + """Test change channel commands.""" + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} + ) + + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + data = hass.data[DOMAIN][entry.entry_id] + change_channel_mock = data._client.change_channel + + # Tell the remote to change channels + await hass.services.async_call( + DOMAIN, + SERVICE_CHANGE_CHANNEL, + {ATTR_ENTITY_ID: ENTITY_REMOTE, ATTR_CHANNEL: 100}, + blocking=True, + ) + await hass.async_block_till_done() + + change_channel_mock.assert_awaited_once_with(100) + + +async def test_sync(mock_hc, mock_write_config, hass): + """Test the sync command.""" + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} + ) + + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + data = hass.data[DOMAIN][entry.entry_id] + sync_mock = data._client.sync + + # Tell the remote to change channels + await hass.services.async_call( + DOMAIN, + SERVICE_SYNC, + {ATTR_ENTITY_ID: ENTITY_REMOTE}, + blocking=True, + ) + await hass.async_block_till_done() + + sync_mock.assert_awaited_once() + mock_write_config.assert_called() + + +async def _send_commands_and_wait(hass, service_data): + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + service_data, + blocking=True, + ) + await hass.async_block_till_done() diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py index 994188eb62a..52ef71fc8bc 100644 --- a/tests/components/harmony/test_config_flow.py +++ b/tests/components/harmony/test_config_flow.py @@ -1,10 +1,11 @@ """Test the Logitech Harmony Hub config flow.""" +from unittest.mock import AsyncMock, MagicMock, patch + from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.harmony.config_flow import CannotConnect from homeassistant.components.harmony.const import DOMAIN, PREVIOUS_ACTIVE_ACTIVITY from homeassistant.const import CONF_HOST, CONF_NAME -from tests.async_mock import AsyncMock, MagicMock, PropertyMock, patch from tests.common import MockConfigEntry @@ -16,23 +17,6 @@ def _get_mock_harmonyapi(connect=None, close=None): return harmonyapi_mock -def _get_mock_harmonyclient(): - harmonyclient_mock = MagicMock() - type(harmonyclient_mock).connect = AsyncMock() - type(harmonyclient_mock).close = AsyncMock() - type(harmonyclient_mock).get_activity_name = MagicMock(return_value="Watch TV") - type(harmonyclient_mock.hub_config).activities = PropertyMock( - return_value=[{"name": "Watch TV", "id": 123}] - ) - type(harmonyclient_mock.hub_config).devices = PropertyMock( - return_value=[{"name": "My TV", "id": 1234}] - ) - type(harmonyclient_mock.hub_config).info = PropertyMock(return_value={}) - type(harmonyclient_mock.hub_config).hub_state = PropertyMock(return_value={}) - - return harmonyclient_mock - - async def test_user_form(hass): """Test we get the user form.""" await setup.async_setup_component(hass, "persistent_notification", {}) @@ -65,49 +49,6 @@ async def test_user_form(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_import(hass): - """Test we get the form with import source.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - harmonyapi = _get_mock_harmonyapi(connect=True) - with patch( - "homeassistant.components.harmony.util.HarmonyAPI", - return_value=harmonyapi, - ), patch( - "homeassistant.components.harmony.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.harmony.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - "host": "1.2.3.4", - "name": "friend", - "activity": "Watch TV", - "delay_secs": 0.9, - "unique_id": "555234534543", - }, - ) - await hass.async_block_till_done() - - assert result["result"].unique_id == "555234534543" - assert result["type"] == "create_entry" - assert result["title"] == "friend" - assert result["data"] == { - "host": "1.2.3.4", - "name": "friend", - "activity": "Watch TV", - "delay_secs": 0.9, - } - # It is not possible to import options at this time - # so they end up in the config entry data and are - # used a fallback when they are not in options - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - async def test_form_ssdp(hass): """Test we get the form with ssdp source.""" await setup.async_setup_component(hass, "persistent_notification", {}) @@ -212,9 +153,8 @@ async def test_form_cannot_connect(hass): assert result2["errors"] == {"base": "cannot_connect"} -async def test_options_flow(hass): +async def test_options_flow(hass, mock_hc): """Test config flow options.""" - config_entry = MockConfigEntry( domain=DOMAIN, unique_id="abcde12345", @@ -222,19 +162,13 @@ async def test_options_flow(hass): options={"activity": "Watch TV", "delay_secs": 0.5}, ) - harmony_client = _get_mock_harmonyclient() - - with patch( - "aioharmony.harmonyapi.HarmonyClient", - return_value=harmony_client, - ), patch("homeassistant.components.harmony.remote.HarmonyRemote.write_config_file"): - config_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - result = await hass.config_entries.options.async_init(config_entry.entry_id) - await hass.async_block_till_done() - assert await hass.config_entries.async_unload(config_entry.entry_id) - await hass.async_block_till_done() + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(config_entry.entry_id) + await hass.async_block_till_done() + assert await hass.config_entries.async_unload(config_entry.entry_id) + await hass.async_block_till_done() assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "init" diff --git a/tests/components/harmony/test_connection_changes.py b/tests/components/harmony/test_connection_changes.py new file mode 100644 index 00000000000..15d46298855 --- /dev/null +++ b/tests/components/harmony/test_connection_changes.py @@ -0,0 +1,67 @@ +"""Test the Logitech Harmony Hub entities with connection state changes.""" + +from datetime import timedelta + +from homeassistant.components.harmony.const import DOMAIN +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.util import utcnow + +from .const import ENTITY_PLAY_MUSIC, ENTITY_REMOTE, ENTITY_WATCH_TV, HUB_NAME + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_connection_state_changes(mock_hc, hass, mock_write_config): + """Ensure connection changes are reflected in the switch states.""" + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} + ) + + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + data = hass.data[DOMAIN][entry.entry_id] + + # mocks start with current activity == Watch TV + assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) + + data._disconnected() + await hass.async_block_till_done() + + # Entities do not immediately show as unavailable + assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) + + future_time = utcnow() + timedelta(seconds=10) + async_fire_time_changed(hass, future_time) + await hass.async_block_till_done() + assert hass.states.is_state(ENTITY_REMOTE, STATE_UNAVAILABLE) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_UNAVAILABLE) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_UNAVAILABLE) + + data._connected() + await hass.async_block_till_done() + + assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) + + data._disconnected() + data._connected() + future_time = utcnow() + timedelta(seconds=10) + async_fire_time_changed(hass, future_time) + + await hass.async_block_till_done() + assert hass.states.is_state(ENTITY_REMOTE, STATE_ON) + assert hass.states.is_state(ENTITY_WATCH_TV, STATE_ON) + assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) diff --git a/tests/components/harmony/test_subscriber.py b/tests/components/harmony/test_subscriber.py new file mode 100644 index 00000000000..5c357bef825 --- /dev/null +++ b/tests/components/harmony/test_subscriber.py @@ -0,0 +1,143 @@ +"""Test the HarmonySubscriberMixin class.""" + +import asyncio +from unittest.mock import AsyncMock, MagicMock + +from homeassistant.components.harmony.subscriber import ( + HarmonyCallback, + HarmonySubscriberMixin, +) + +_NO_PARAM_CALLBACKS = { + "connected": "_connected", + "disconnected": "_disconnected", + "config_updated": "_config_updated", +} + +_ACTIVITY_CALLBACKS = { + "activity_starting": "_activity_starting", + "activity_started": "_activity_started", +} + +_ALL_CALLBACK_NAMES = list(_NO_PARAM_CALLBACKS.keys()) + list( + _ACTIVITY_CALLBACKS.keys() +) + +_ACTIVITY_TUPLE = ("not", "used") + + +async def test_no_callbacks(hass): + """Ensure we handle no subscriptions.""" + subscriber = HarmonySubscriberMixin(hass) + _call_all_callbacks(subscriber) + await hass.async_block_till_done() + + +async def test_empty_callbacks(hass): + """Ensure we handle a missing callback in a subscription.""" + subscriber = HarmonySubscriberMixin(hass) + + callbacks = {k: None for k in _ALL_CALLBACK_NAMES} + subscriber.async_subscribe(HarmonyCallback(**callbacks)) + _call_all_callbacks(subscriber) + await hass.async_block_till_done() + + +async def test_async_callbacks(hass): + """Ensure we handle async callbacks.""" + subscriber = HarmonySubscriberMixin(hass) + + callbacks = {k: AsyncMock() for k in _ALL_CALLBACK_NAMES} + subscriber.async_subscribe(HarmonyCallback(**callbacks)) + _call_all_callbacks(subscriber) + await hass.async_block_till_done() + + for callback_name in _NO_PARAM_CALLBACKS.keys(): + callback_mock = callbacks[callback_name] + callback_mock.assert_awaited_once() + + for callback_name in _ACTIVITY_CALLBACKS.keys(): + callback_mock = callbacks[callback_name] + callback_mock.assert_awaited_once_with(_ACTIVITY_TUPLE) + + +async def test_long_async_callbacks(hass): + """Ensure we handle async callbacks that may have sleeps.""" + subscriber = HarmonySubscriberMixin(hass) + + blocker_event = asyncio.Event() + notifier_event_one = asyncio.Event() + notifier_event_two = asyncio.Event() + + async def blocks_until_notified(): + await blocker_event.wait() + notifier_event_one.set() + + async def notifies_when_called(): + notifier_event_two.set() + + callbacks_one = {k: blocks_until_notified for k in _ALL_CALLBACK_NAMES} + callbacks_two = {k: notifies_when_called for k in _ALL_CALLBACK_NAMES} + subscriber.async_subscribe(HarmonyCallback(**callbacks_one)) + subscriber.async_subscribe(HarmonyCallback(**callbacks_two)) + + subscriber._connected() + await notifier_event_two.wait() + blocker_event.set() + await notifier_event_one.wait() + + +async def test_callbacks(hass): + """Ensure we handle non-async callbacks.""" + subscriber = HarmonySubscriberMixin(hass) + + callbacks = {k: MagicMock() for k in _ALL_CALLBACK_NAMES} + subscriber.async_subscribe(HarmonyCallback(**callbacks)) + _call_all_callbacks(subscriber) + await hass.async_block_till_done() + + for callback_name in _NO_PARAM_CALLBACKS.keys(): + callback_mock = callbacks[callback_name] + callback_mock.assert_called_once() + + for callback_name in _ACTIVITY_CALLBACKS.keys(): + callback_mock = callbacks[callback_name] + callback_mock.assert_called_once_with(_ACTIVITY_TUPLE) + + +async def test_subscribe_unsubscribe(hass): + """Ensure we handle subscriptions and unsubscriptions correctly.""" + subscriber = HarmonySubscriberMixin(hass) + + callback_one = {k: MagicMock() for k in _ALL_CALLBACK_NAMES} + unsub_one = subscriber.async_subscribe(HarmonyCallback(**callback_one)) + callback_two = {k: MagicMock() for k in _ALL_CALLBACK_NAMES} + _ = subscriber.async_subscribe(HarmonyCallback(**callback_two)) + callback_three = {k: MagicMock() for k in _ALL_CALLBACK_NAMES} + unsub_three = subscriber.async_subscribe(HarmonyCallback(**callback_three)) + + unsub_one() + unsub_three() + + _call_all_callbacks(subscriber) + await hass.async_block_till_done() + + for callback_name in _NO_PARAM_CALLBACKS.keys(): + callback_one[callback_name].assert_not_called() + callback_two[callback_name].assert_called_once() + callback_three[callback_name].assert_not_called() + + for callback_name in _ACTIVITY_CALLBACKS.keys(): + callback_one[callback_name].assert_not_called() + callback_two[callback_name].assert_called_once_with(_ACTIVITY_TUPLE) + callback_three[callback_name].assert_not_called() + + +def _call_all_callbacks(subscriber): + for callback_method in _NO_PARAM_CALLBACKS.values(): + to_call = getattr(subscriber, callback_method) + to_call() + + for callback_method in _ACTIVITY_CALLBACKS.values(): + to_call = getattr(subscriber, callback_method) + to_call(_ACTIVITY_TUPLE) diff --git a/tests/components/hassio/conftest.py b/tests/components/hassio/conftest.py index f52a825ca29..1442d133f1e 100644 --- a/tests/components/hassio/conftest.py +++ b/tests/components/hassio/conftest.py @@ -1,5 +1,6 @@ """Fixtures for Hass.io.""" import os +from unittest.mock import Mock, patch import pytest @@ -9,8 +10,6 @@ from homeassistant.setup import async_setup_component from . import HASSIO_TOKEN -from tests.async_mock import Mock, patch - @pytest.fixture def hassio_env(): diff --git a/tests/components/hassio/test_addon_panel.py b/tests/components/hassio/test_addon_panel.py index 9b11fd6dfc2..3f6db4dc430 100644 --- a/tests/components/hassio/test_addon_panel.py +++ b/tests/components/hassio/test_addon_panel.py @@ -1,10 +1,10 @@ """Test add-on panel.""" +from unittest.mock import patch + import pytest from homeassistant.setup import async_setup_component -from tests.async_mock import patch - @pytest.fixture(autouse=True) def mock_all(aioclient_mock): diff --git a/tests/components/hassio/test_auth.py b/tests/components/hassio/test_auth.py index 3d6a339082c..a533d468069 100644 --- a/tests/components/hassio/test_auth.py +++ b/tests/components/hassio/test_auth.py @@ -1,8 +1,8 @@ """The tests for the hassio component.""" -from homeassistant.auth.providers.homeassistant import InvalidAuth +from unittest.mock import Mock, patch -from tests.async_mock import Mock, patch +from homeassistant.auth.providers.homeassistant import InvalidAuth async def test_auth_success(hass, hassio_client_supervisor): diff --git a/tests/components/hassio/test_discovery.py b/tests/components/hassio/test_discovery.py index 3bb97a6662e..c23ee40de6e 100644 --- a/tests/components/hassio/test_discovery.py +++ b/tests/components/hassio/test_discovery.py @@ -1,10 +1,10 @@ """Test config flow.""" +from unittest.mock import Mock, patch + from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch - async def test_hassio_discovery_startup(hass, aioclient_mock, hassio_client): """Test startup and discovery after event.""" diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index ea087cdc620..2ec964d8e8b 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -1,12 +1,11 @@ """The tests for the hassio component.""" import asyncio +from unittest.mock import patch import pytest from homeassistant.components.hassio.http import _need_auth -from tests.async_mock import patch - async def test_forward_request(hassio_client, aioclient_mock): """Test fetching normal path.""" diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 214551bc3b7..7ed24dca457 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -1,5 +1,6 @@ """The tests for the hassio component.""" import os +from unittest.mock import patch import pytest @@ -8,8 +9,6 @@ from homeassistant.components import frontend from homeassistant.components.hassio import STORAGE_KEY from homeassistant.setup import async_setup_component -from tests.async_mock import patch - MOCK_ENVIRON = {"HASSIO": "127.0.0.1", "HASSIO_TOKEN": "abcdefgh"} diff --git a/tests/components/hassio/test_system_health.py b/tests/components/hassio/test_system_health.py index cd6cb2d939f..8fa610b5442 100644 --- a/tests/components/hassio/test_system_health.py +++ b/tests/components/hassio/test_system_health.py @@ -1,6 +1,7 @@ """Test hassio system health.""" import asyncio import os +from unittest.mock import patch from aiohttp import ClientError @@ -8,7 +9,6 @@ from homeassistant.setup import async_setup_component from .test_init import MOCK_ENVIRON -from tests.async_mock import patch from tests.common import get_system_health_info @@ -60,7 +60,7 @@ async def test_hassio_system_health(hass, aioclient_mock): "host_os": "Home Assistant OS 5.9", "installed_addons": "Awesome Addon (1.0.0)", "supervisor_api": "ok", - "supervisor_version": "2020.11.1", + "supervisor_version": "supervisor-2020.11.1", "supported": True, "update_channel": "stable", "version_api": "ok", diff --git a/tests/components/hddtemp/test_sensor.py b/tests/components/hddtemp/test_sensor.py index 4062d737ea2..7c7ac5ef1e8 100644 --- a/tests/components/hddtemp/test_sensor.py +++ b/tests/components/hddtemp/test_sensor.py @@ -1,12 +1,11 @@ """The tests for the hddtemp platform.""" import socket -import unittest +from unittest.mock import patch + +import pytest from homeassistant.const import TEMP_CELSIUS -from homeassistant.setup import setup_component - -from tests.async_mock import patch -from tests.common import get_test_home_assistant +from homeassistant.setup import async_setup_component VALID_CONFIG_MINIMAL = {"sensor": {"platform": "hddtemp"}} @@ -24,10 +23,37 @@ VALID_CONFIG_MULTIPLE_DISKS = { } } -VALID_CONFIG_HOST = {"sensor": {"platform": "hddtemp", "host": "alice.local"}} +VALID_CONFIG_HOST_REFUSED = {"sensor": {"platform": "hddtemp", "host": "alice.local"}} VALID_CONFIG_HOST_UNREACHABLE = {"sensor": {"platform": "hddtemp", "host": "bob.local"}} +REFERENCE = { + "/dev/sda1": { + "device": "/dev/sda1", + "temperature": "29", + "unit_of_measurement": TEMP_CELSIUS, + "model": "WDC WD30EZRX-12DC0B0", + }, + "/dev/sdb1": { + "device": "/dev/sdb1", + "temperature": "32", + "unit_of_measurement": TEMP_CELSIUS, + "model": "WDC WD15EADS-11P7B2", + }, + "/dev/sdc1": { + "device": "/dev/sdc1", + "temperature": "29", + "unit_of_measurement": TEMP_CELSIUS, + "model": "WDC WD20EARX-22MMMB0", + }, + "/dev/sdd1": { + "device": "/dev/sdd1", + "temperature": "32", + "unit_of_measurement": TEMP_CELSIUS, + "model": "WDC WD15EARS-00Z5B1", + }, +} + class TelnetMock: """Mock class for the telnetlib.Telnet object.""" @@ -54,51 +80,91 @@ class TelnetMock: return self.sample_data -class TestHDDTempSensor(unittest.TestCase): - """Test the hddtemp sensor.""" +@pytest.fixture +def telnetmock(): + """Mock telnet.""" + with patch("telnetlib.Telnet", new=TelnetMock): + yield - def setUp(self): - """Set up things to run when tests begin.""" - self.hass = get_test_home_assistant() - self.config = VALID_CONFIG_ONE_DISK - self.reference = { - "/dev/sda1": { - "device": "/dev/sda1", - "temperature": "29", - "unit_of_measurement": TEMP_CELSIUS, - "model": "WDC WD30EZRX-12DC0B0", - }, - "/dev/sdb1": { - "device": "/dev/sdb1", - "temperature": "32", - "unit_of_measurement": TEMP_CELSIUS, - "model": "WDC WD15EADS-11P7B2", - }, - "/dev/sdc1": { - "device": "/dev/sdc1", - "temperature": "29", - "unit_of_measurement": TEMP_CELSIUS, - "model": "WDC WD20EARX-22MMMB0", - }, - "/dev/sdd1": { - "device": "/dev/sdd1", - "temperature": "32", - "unit_of_measurement": TEMP_CELSIUS, - "model": "WDC WD15EARS-00Z5B1", - }, - } - self.addCleanup(self.hass.stop) - @patch("telnetlib.Telnet", new=TelnetMock) - def test_hddtemp_min_config(self): - """Test minimal hddtemp configuration.""" - assert setup_component(self.hass, "sensor", VALID_CONFIG_MINIMAL) - self.hass.block_till_done() +async def test_hddtemp_min_config(hass, telnetmock): + """Test minimal hddtemp configuration.""" + assert await async_setup_component(hass, "sensor", VALID_CONFIG_MINIMAL) + await hass.async_block_till_done() - entity = self.hass.states.all()[0].entity_id - state = self.hass.states.get(entity) + entity_id = hass.states.async_all()[0].entity_id + state = hass.states.get(entity_id) - reference = self.reference[state.attributes.get("device")] + reference = REFERENCE[state.attributes.get("device")] + + assert state.state == reference["temperature"] + assert state.attributes.get("device") == reference["device"] + assert state.attributes.get("model") == reference["model"] + assert ( + state.attributes.get("unit_of_measurement") == reference["unit_of_measurement"] + ) + assert ( + state.attributes.get("friendly_name") == f"HD Temperature {reference['device']}" + ) + + +async def test_hddtemp_rename_config(hass, telnetmock): + """Test hddtemp configuration with different name.""" + assert await async_setup_component(hass, "sensor", VALID_CONFIG_NAME) + await hass.async_block_till_done() + + entity_id = hass.states.async_all()[0].entity_id + state = hass.states.get(entity_id) + + reference = REFERENCE[state.attributes.get("device")] + + assert state.attributes.get("friendly_name") == f"FooBar {reference['device']}" + + +async def test_hddtemp_one_disk(hass, telnetmock): + """Test hddtemp one disk configuration.""" + assert await async_setup_component(hass, "sensor", VALID_CONFIG_ONE_DISK) + await hass.async_block_till_done() + + state = hass.states.get("sensor.hd_temperature_dev_sdd1") + + reference = REFERENCE[state.attributes.get("device")] + + assert state.state == reference["temperature"] + assert state.attributes.get("device") == reference["device"] + assert state.attributes.get("model") == reference["model"] + assert ( + state.attributes.get("unit_of_measurement") == reference["unit_of_measurement"] + ) + assert ( + state.attributes.get("friendly_name") == f"HD Temperature {reference['device']}" + ) + + +async def test_hddtemp_wrong_disk(hass, telnetmock): + """Test hddtemp wrong disk configuration.""" + assert await async_setup_component(hass, "sensor", VALID_CONFIG_WRONG_DISK) + await hass.async_block_till_done() + + assert len(hass.states.async_all()) == 1 + state = hass.states.get("sensor.hd_temperature_dev_sdx1") + assert state.attributes.get("friendly_name") == "HD Temperature /dev/sdx1" + + +async def test_hddtemp_multiple_disks(hass, telnetmock): + """Test hddtemp multiple disk configuration.""" + assert await async_setup_component(hass, "sensor", VALID_CONFIG_MULTIPLE_DISKS) + await hass.async_block_till_done() + + for sensor in [ + "sensor.hd_temperature_dev_sda1", + "sensor.hd_temperature_dev_sdb1", + "sensor.hd_temperature_dev_sdc1", + ]: + + state = hass.states.get(sensor) + + reference = REFERENCE[state.attributes.get("device")] assert state.state == reference["temperature"] assert state.attributes.get("device") == reference["device"] @@ -112,89 +178,16 @@ class TestHDDTempSensor(unittest.TestCase): == f"HD Temperature {reference['device']}" ) - @patch("telnetlib.Telnet", new=TelnetMock) - def test_hddtemp_rename_config(self): - """Test hddtemp configuration with different name.""" - assert setup_component(self.hass, "sensor", VALID_CONFIG_NAME) - self.hass.block_till_done() - entity = self.hass.states.all()[0].entity_id - state = self.hass.states.get(entity) +async def test_hddtemp_host_refused(hass, telnetmock): + """Test hddtemp if host is refused.""" + assert await async_setup_component(hass, "sensor", VALID_CONFIG_HOST_REFUSED) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 0 - reference = self.reference[state.attributes.get("device")] - assert state.attributes.get("friendly_name") == f"FooBar {reference['device']}" - - @patch("telnetlib.Telnet", new=TelnetMock) - def test_hddtemp_one_disk(self): - """Test hddtemp one disk configuration.""" - assert setup_component(self.hass, "sensor", VALID_CONFIG_ONE_DISK) - self.hass.block_till_done() - - state = self.hass.states.get("sensor.hd_temperature_dev_sdd1") - - reference = self.reference[state.attributes.get("device")] - - assert state.state == reference["temperature"] - assert state.attributes.get("device") == reference["device"] - assert state.attributes.get("model") == reference["model"] - assert ( - state.attributes.get("unit_of_measurement") - == reference["unit_of_measurement"] - ) - assert ( - state.attributes.get("friendly_name") - == f"HD Temperature {reference['device']}" - ) - - @patch("telnetlib.Telnet", new=TelnetMock) - def test_hddtemp_wrong_disk(self): - """Test hddtemp wrong disk configuration.""" - assert setup_component(self.hass, "sensor", VALID_CONFIG_WRONG_DISK) - self.hass.block_till_done() - - assert len(self.hass.states.all()) == 1 - state = self.hass.states.get("sensor.hd_temperature_dev_sdx1") - assert state.attributes.get("friendly_name") == "HD Temperature /dev/sdx1" - - @patch("telnetlib.Telnet", new=TelnetMock) - def test_hddtemp_multiple_disks(self): - """Test hddtemp multiple disk configuration.""" - assert setup_component(self.hass, "sensor", VALID_CONFIG_MULTIPLE_DISKS) - self.hass.block_till_done() - - for sensor in [ - "sensor.hd_temperature_dev_sda1", - "sensor.hd_temperature_dev_sdb1", - "sensor.hd_temperature_dev_sdc1", - ]: - - state = self.hass.states.get(sensor) - - reference = self.reference[state.attributes.get("device")] - - assert state.state == reference["temperature"] - assert state.attributes.get("device") == reference["device"] - assert state.attributes.get("model") == reference["model"] - assert ( - state.attributes.get("unit_of_measurement") - == reference["unit_of_measurement"] - ) - assert ( - state.attributes.get("friendly_name") - == f"HD Temperature {reference['device']}" - ) - - @patch("telnetlib.Telnet", new=TelnetMock) - def test_hddtemp_host_refused(self): - """Test hddtemp if host unreachable.""" - assert setup_component(self.hass, "sensor", VALID_CONFIG_HOST) - self.hass.block_till_done() - assert len(self.hass.states.all()) == 0 - - @patch("telnetlib.Telnet", new=TelnetMock) - def test_hddtemp_host_unreachable(self): - """Test hddtemp if host unreachable.""" - assert setup_component(self.hass, "sensor", VALID_CONFIG_HOST_UNREACHABLE) - self.hass.block_till_done() - assert len(self.hass.states.all()) == 0 +async def test_hddtemp_host_unreachable(hass, telnetmock): + """Test hddtemp if host unreachable.""" + assert await async_setup_component(hass, "sensor", VALID_CONFIG_HOST_UNREACHABLE) + await hass.async_block_till_done() + assert len(hass.states.async_all()) == 0 diff --git a/tests/components/heos/conftest.py b/tests/components/heos/conftest.py index 86be36e8188..fa7615e2de8 100644 --- a/tests/components/heos/conftest.py +++ b/tests/components/heos/conftest.py @@ -1,5 +1,6 @@ """Configuration for HEOS tests.""" from typing import Dict, Sequence +from unittest.mock import Mock, patch as patch from pyheos import Dispatcher, Heos, HeosPlayer, HeosSource, InputSource, const import pytest @@ -8,7 +9,6 @@ from homeassistant.components import ssdp from homeassistant.components.heos import DOMAIN from homeassistant.const import CONF_HOST -from tests.async_mock import Mock, patch as patch from tests.common import MockConfigEntry diff --git a/tests/components/heos/test_config_flow.py b/tests/components/heos/test_config_flow.py index bcc2ed67b29..e62578e5108 100644 --- a/tests/components/heos/test_config_flow.py +++ b/tests/components/heos/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the Heos config flow module.""" +from unittest.mock import patch from urllib.parse import urlparse from pyheos import HeosError @@ -10,8 +11,6 @@ from homeassistant.components.heos.const import DATA_DISCOVERED_HOSTS, DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP from homeassistant.const import CONF_HOST -from tests.async_mock import patch - async def test_flow_aborts_already_setup(hass, config_entry): """Test flow aborts when entry already setup.""" diff --git a/tests/components/heos/test_init.py b/tests/components/heos/test_init.py index a32ea5dd08c..6edbf7d8543 100644 --- a/tests/components/heos/test_init.py +++ b/tests/components/heos/test_init.py @@ -1,5 +1,6 @@ """Tests for the init module.""" import asyncio +from unittest.mock import Mock, patch from pyheos import CommandFailedError, HeosError, const import pytest @@ -19,8 +20,6 @@ from homeassistant.const import CONF_HOST from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch - async def test_async_setup_creates_entry(hass, config): """Test component setup creates entry from config.""" diff --git a/tests/components/heos/test_media_player.py b/tests/components/heos/test_media_player.py index eba0eb0f3fb..ef7285ab185 100644 --- a/tests/components/heos/test_media_player.py +++ b/tests/components/heos/test_media_player.py @@ -243,7 +243,7 @@ async def test_updates_from_players_changed_new_ids( event = asyncio.Event() # Assert device registry matches current id - assert device_registry.async_get_device({(DOMAIN, 1)}, []) + assert device_registry.async_get_device({(DOMAIN, 1)}) # Assert entity registry matches current id assert ( entity_registry.async_get_entity_id(MEDIA_PLAYER_DOMAIN, DOMAIN, "1") @@ -264,7 +264,7 @@ async def test_updates_from_players_changed_new_ids( # Assert device registry identifiers were updated assert len(device_registry.devices) == 1 - assert device_registry.async_get_device({(DOMAIN, 101)}, []) + assert device_registry.async_get_device({(DOMAIN, 101)}) # Assert entity registry unique id was updated assert len(entity_registry.entities) == 1 assert ( diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index 386dbbdf0ee..b2fcef715f1 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -1,5 +1,6 @@ """The test for the here_travel_time sensor platform.""" import logging +from unittest.mock import patch import urllib import herepy @@ -42,7 +43,6 @@ from homeassistant.const import ATTR_ICON, EVENT_HOMEASSISTANT_START from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import async_fire_time_changed, load_fixture DOMAIN = "sensor" diff --git a/tests/components/hisense_aehw4a1/test_init.py b/tests/components/hisense_aehw4a1/test_init.py index bf3c9d2c6a4..4add153ee94 100644 --- a/tests/components/hisense_aehw4a1/test_init.py +++ b/tests/components/hisense_aehw4a1/test_init.py @@ -1,12 +1,12 @@ """Tests for the Hisense AEH-W4A1 init file.""" +from unittest.mock import patch + from pyaehw4a1 import exceptions from homeassistant import config_entries, data_entry_flow from homeassistant.components import hisense_aehw4a1 from homeassistant.setup import async_setup_component -from tests.async_mock import patch - async def test_creating_entry_sets_up_climate_discovery(hass): """Test setting up Hisense AEH-W4A1 loads the climate component.""" diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 0b35d5194fb..87e18305b8a 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -4,6 +4,7 @@ from copy import copy from datetime import timedelta import json import unittest +from unittest.mock import patch, sentinel from homeassistant.components import history, recorder from homeassistant.components.recorder.models import process_timestamp @@ -12,7 +13,6 @@ from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component, setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch, sentinel from tests.common import ( get_test_home_assistant, init_recorder_component, diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index db6d7476912..62e3959f4ad 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -3,6 +3,7 @@ from datetime import datetime, timedelta from os import path import unittest +from unittest.mock import patch import pytest import pytz @@ -16,7 +17,6 @@ from homeassistant.helpers.template import Template from homeassistant.setup import async_setup_component, setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import get_test_home_assistant, init_recorder_component diff --git a/tests/components/hlk_sw16/test_config_flow.py b/tests/components/hlk_sw16/test_config_flow.py index ea637c805cd..6a13fae70dc 100644 --- a/tests/components/hlk_sw16/test_config_flow.py +++ b/tests/components/hlk_sw16/test_config_flow.py @@ -1,11 +1,10 @@ """Test the Hi-Link HLK-SW16 config flow.""" import asyncio +from unittest.mock import patch from homeassistant import config_entries, setup from homeassistant.components.hlk_sw16.const import DOMAIN -from tests.async_mock import patch - class MockSW16Client: """Class to mock the SW16Client client.""" diff --git a/tests/components/home_connect/test_config_flow.py b/tests/components/home_connect/test_config_flow.py index 5c94f8b3362..2852dc4fb57 100644 --- a/tests/components/home_connect/test_config_flow.py +++ b/tests/components/home_connect/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Home Connect config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.home_connect.const import ( DOMAIN, @@ -8,8 +10,6 @@ from homeassistant.components.home_connect.const import ( from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow -from tests.async_mock import patch - CLIENT_ID = "1234" CLIENT_SECRET = "5678" diff --git a/tests/components/homeassistant/test_init.py b/tests/components/homeassistant/test_init.py index ca2f116a06a..ef830c7ee77 100644 --- a/tests/components/homeassistant/test_init.py +++ b/tests/components/homeassistant/test_init.py @@ -2,6 +2,7 @@ # pylint: disable=protected-access import asyncio import unittest +from unittest.mock import Mock, patch import pytest import voluptuous as vol @@ -32,7 +33,6 @@ from homeassistant.exceptions import HomeAssistantError, Unauthorized from homeassistant.helpers import entity from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch from tests.common import ( async_capture_events, async_mock_service, diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py index 8f47d891f9f..30985432718 100644 --- a/tests/components/homeassistant/test_scene.py +++ b/tests/components/homeassistant/test_scene.py @@ -1,4 +1,6 @@ """Test Home Assistant scenes.""" +from unittest.mock import patch + import pytest import voluptuous as vol @@ -6,7 +8,6 @@ from homeassistant.components.homeassistant import scene as ha_scene from homeassistant.components.homeassistant.scene import EVENT_SCENE_RELOADED from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import async_mock_service diff --git a/tests/components/homeassistant/triggers/test_homeassistant.py b/tests/components/homeassistant/triggers/test_homeassistant.py index 9272c3620af..7ff7e566db0 100644 --- a/tests/components/homeassistant/triggers/test_homeassistant.py +++ b/tests/components/homeassistant/triggers/test_homeassistant.py @@ -1,9 +1,10 @@ """The tests for the Event automation.""" +from unittest.mock import AsyncMock, patch + import homeassistant.components.automation as automation from homeassistant.core import CoreState from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, patch from tests.common import async_mock_service diff --git a/tests/components/homeassistant/triggers/test_numeric_state.py b/tests/components/homeassistant/triggers/test_numeric_state.py index 326990e12c6..b9696fffe06 100644 --- a/tests/components/homeassistant/triggers/test_numeric_state.py +++ b/tests/components/homeassistant/triggers/test_numeric_state.py @@ -1,5 +1,6 @@ """The tests for numeric state automation.""" from datetime import timedelta +from unittest.mock import patch import pytest import voluptuous as vol @@ -13,7 +14,6 @@ from homeassistant.core import Context from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import ( assert_setup_component, async_fire_time_changed, @@ -29,12 +29,27 @@ def calls(hass): @pytest.fixture(autouse=True) -def setup_comp(hass): +async def setup_comp(hass): """Initialize components.""" mock_component(hass, "group") + await async_setup_component( + hass, + "input_number", + { + "input_number": { + "value_3": {"min": 0, "max": 255, "initial": 3}, + "value_5": {"min": 0, "max": 255, "initial": 5}, + "value_8": {"min": 0, "max": 255, "initial": 8}, + "value_10": {"min": 0, "max": 255, "initial": 10}, + "value_12": {"min": 0, "max": 255, "initial": 12}, + "value_100": {"min": 0, "max": 255, "initial": 100}, + } + }, + ) -async def test_if_not_fires_on_entity_removal(hass, calls): +@pytest.mark.parametrize("below", (10, "input_number.value_10")) +async def test_if_not_fires_on_entity_removal(hass, calls, below): """Test the firing with removed entity.""" hass.states.async_set("test.entity", 11) @@ -46,7 +61,7 @@ async def test_if_not_fires_on_entity_removal(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "below": 10, + "below": below, }, "action": {"service": "test.automation"}, } @@ -59,7 +74,8 @@ async def test_if_not_fires_on_entity_removal(hass, calls): assert len(calls) == 0 -async def test_if_fires_on_entity_change_below(hass, calls): +@pytest.mark.parametrize("below", (10, "input_number.value_10")) +async def test_if_fires_on_entity_change_below(hass, calls, below): """Test the firing with changed entity.""" hass.states.async_set("test.entity", 11) await hass.async_block_till_done() @@ -73,7 +89,7 @@ async def test_if_fires_on_entity_change_below(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "below": 10, + "below": below, }, "action": {"service": "test.automation"}, } @@ -99,7 +115,8 @@ async def test_if_fires_on_entity_change_below(hass, calls): assert len(calls) == 1 -async def test_if_fires_on_entity_change_over_to_below(hass, calls): +@pytest.mark.parametrize("below", (10, "input_number.value_10")) +async def test_if_fires_on_entity_change_over_to_below(hass, calls, below): """Test the firing with changed entity.""" hass.states.async_set("test.entity", 11) await hass.async_block_till_done() @@ -112,7 +129,7 @@ async def test_if_fires_on_entity_change_over_to_below(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "below": 10, + "below": below, }, "action": {"service": "test.automation"}, } @@ -125,7 +142,8 @@ async def test_if_fires_on_entity_change_over_to_below(hass, calls): assert len(calls) == 1 -async def test_if_fires_on_entities_change_over_to_below(hass, calls): +@pytest.mark.parametrize("below", (10, "input_number.value_10")) +async def test_if_fires_on_entities_change_over_to_below(hass, calls, below): """Test the firing with changed entities.""" hass.states.async_set("test.entity_1", 11) hass.states.async_set("test.entity_2", 11) @@ -139,7 +157,7 @@ async def test_if_fires_on_entities_change_over_to_below(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": ["test.entity_1", "test.entity_2"], - "below": 10, + "below": below, }, "action": {"service": "test.automation"}, } @@ -155,7 +173,8 @@ async def test_if_fires_on_entities_change_over_to_below(hass, calls): assert len(calls) == 2 -async def test_if_not_fires_on_entity_change_below_to_below(hass, calls): +@pytest.mark.parametrize("below", (10, "input_number.value_10")) +async def test_if_not_fires_on_entity_change_below_to_below(hass, calls, below): """Test the firing with changed entity.""" context = Context() hass.states.async_set("test.entity", 11) @@ -169,7 +188,7 @@ async def test_if_not_fires_on_entity_change_below_to_below(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "below": 10, + "below": below, }, "action": {"service": "test.automation"}, } @@ -193,7 +212,8 @@ async def test_if_not_fires_on_entity_change_below_to_below(hass, calls): assert len(calls) == 1 -async def test_if_not_below_fires_on_entity_change_to_equal(hass, calls): +@pytest.mark.parametrize("below", (10, "input_number.value_10")) +async def test_if_not_below_fires_on_entity_change_to_equal(hass, calls, below): """Test the firing with changed entity.""" hass.states.async_set("test.entity", 11) await hass.async_block_till_done() @@ -206,7 +226,7 @@ async def test_if_not_below_fires_on_entity_change_to_equal(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "below": 10, + "below": below, }, "action": {"service": "test.automation"}, } @@ -219,7 +239,8 @@ async def test_if_not_below_fires_on_entity_change_to_equal(hass, calls): assert len(calls) == 0 -async def test_if_fires_on_initial_entity_below(hass, calls): +@pytest.mark.parametrize("below", (10, "input_number.value_10")) +async def test_if_fires_on_initial_entity_below(hass, calls, below): """Test the firing when starting with a match.""" hass.states.async_set("test.entity", 9) await hass.async_block_till_done() @@ -232,7 +253,7 @@ async def test_if_fires_on_initial_entity_below(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "below": 10, + "below": below, }, "action": {"service": "test.automation"}, } @@ -245,7 +266,8 @@ async def test_if_fires_on_initial_entity_below(hass, calls): assert len(calls) == 1 -async def test_if_fires_on_initial_entity_above(hass, calls): +@pytest.mark.parametrize("above", (10, "input_number.value_10")) +async def test_if_fires_on_initial_entity_above(hass, calls, above): """Test the firing when starting with a match.""" hass.states.async_set("test.entity", 11) await hass.async_block_till_done() @@ -258,7 +280,7 @@ async def test_if_fires_on_initial_entity_above(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 10, + "above": above, }, "action": {"service": "test.automation"}, } @@ -271,7 +293,8 @@ async def test_if_fires_on_initial_entity_above(hass, calls): assert len(calls) == 1 -async def test_if_fires_on_entity_change_above(hass, calls): +@pytest.mark.parametrize("above", (10, "input_number.value_10")) +async def test_if_fires_on_entity_change_above(hass, calls, above): """Test the firing with changed entity.""" hass.states.async_set("test.entity", 9) await hass.async_block_till_done() @@ -284,7 +307,7 @@ async def test_if_fires_on_entity_change_above(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 10, + "above": above, }, "action": {"service": "test.automation"}, } @@ -296,7 +319,8 @@ async def test_if_fires_on_entity_change_above(hass, calls): assert len(calls) == 1 -async def test_if_fires_on_entity_change_below_to_above(hass, calls): +@pytest.mark.parametrize("above", (10, "input_number.value_10")) +async def test_if_fires_on_entity_change_below_to_above(hass, calls, above): """Test the firing with changed entity.""" # set initial state hass.states.async_set("test.entity", 9) @@ -310,7 +334,7 @@ async def test_if_fires_on_entity_change_below_to_above(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 10, + "above": above, }, "action": {"service": "test.automation"}, } @@ -323,7 +347,8 @@ async def test_if_fires_on_entity_change_below_to_above(hass, calls): assert len(calls) == 1 -async def test_if_not_fires_on_entity_change_above_to_above(hass, calls): +@pytest.mark.parametrize("above", (10, "input_number.value_10")) +async def test_if_not_fires_on_entity_change_above_to_above(hass, calls, above): """Test the firing with changed entity.""" # set initial state hass.states.async_set("test.entity", 9) @@ -337,7 +362,7 @@ async def test_if_not_fires_on_entity_change_above_to_above(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 10, + "above": above, }, "action": {"service": "test.automation"}, } @@ -355,7 +380,8 @@ async def test_if_not_fires_on_entity_change_above_to_above(hass, calls): assert len(calls) == 1 -async def test_if_not_above_fires_on_entity_change_to_equal(hass, calls): +@pytest.mark.parametrize("above", (10, "input_number.value_10")) +async def test_if_not_above_fires_on_entity_change_to_equal(hass, calls, above): """Test the firing with changed entity.""" # set initial state hass.states.async_set("test.entity", 9) @@ -369,7 +395,7 @@ async def test_if_not_above_fires_on_entity_change_to_equal(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 10, + "above": above, }, "action": {"service": "test.automation"}, } @@ -382,7 +408,16 @@ async def test_if_not_above_fires_on_entity_change_to_equal(hass, calls): assert len(calls) == 0 -async def test_if_fires_on_entity_change_below_range(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (5, 10), + (5, "input_number.value_10"), + ("input_number.value_5", 10), + ("input_number.value_5", "input_number.value_10"), + ), +) +async def test_if_fires_on_entity_change_below_range(hass, calls, above, below): """Test the firing with changed entity.""" hass.states.async_set("test.entity", 11) await hass.async_block_till_done() @@ -395,8 +430,8 @@ async def test_if_fires_on_entity_change_below_range(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "below": 10, - "above": 5, + "below": below, + "above": above, }, "action": {"service": "test.automation"}, } @@ -408,7 +443,16 @@ async def test_if_fires_on_entity_change_below_range(hass, calls): assert len(calls) == 1 -async def test_if_fires_on_entity_change_below_above_range(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (5, 10), + (5, "input_number.value_10"), + ("input_number.value_5", 10), + ("input_number.value_5", "input_number.value_10"), + ), +) +async def test_if_fires_on_entity_change_below_above_range(hass, calls, above, below): """Test the firing with changed entity.""" assert await async_setup_component( hass, @@ -418,8 +462,8 @@ async def test_if_fires_on_entity_change_below_above_range(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "below": 10, - "above": 5, + "below": below, + "above": above, }, "action": {"service": "test.automation"}, } @@ -431,7 +475,16 @@ async def test_if_fires_on_entity_change_below_above_range(hass, calls): assert len(calls) == 0 -async def test_if_fires_on_entity_change_over_to_below_range(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (5, 10), + (5, "input_number.value_10"), + ("input_number.value_5", 10), + ("input_number.value_5", "input_number.value_10"), + ), +) +async def test_if_fires_on_entity_change_over_to_below_range(hass, calls, above, below): """Test the firing with changed entity.""" hass.states.async_set("test.entity", 11) await hass.async_block_till_done() @@ -444,8 +497,8 @@ async def test_if_fires_on_entity_change_over_to_below_range(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "below": 10, - "above": 5, + "below": below, + "above": above, }, "action": {"service": "test.automation"}, } @@ -458,7 +511,18 @@ async def test_if_fires_on_entity_change_over_to_below_range(hass, calls): assert len(calls) == 1 -async def test_if_fires_on_entity_change_over_to_below_above_range(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (5, 10), + (5, "input_number.value_10"), + ("input_number.value_5", 10), + ("input_number.value_5", "input_number.value_10"), + ), +) +async def test_if_fires_on_entity_change_over_to_below_above_range( + hass, calls, above, below +): """Test the firing with changed entity.""" hass.states.async_set("test.entity", 11) await hass.async_block_till_done() @@ -471,8 +535,8 @@ async def test_if_fires_on_entity_change_over_to_below_above_range(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "below": 10, - "above": 5, + "below": above, + "above": below, }, "action": {"service": "test.automation"}, } @@ -485,7 +549,8 @@ async def test_if_fires_on_entity_change_over_to_below_above_range(hass, calls): assert len(calls) == 0 -async def test_if_not_fires_if_entity_not_match(hass, calls): +@pytest.mark.parametrize("below", (100, "input_number.value_100")) +async def test_if_not_fires_if_entity_not_match(hass, calls, below): """Test if not fired with non matching entity.""" assert await async_setup_component( hass, @@ -495,7 +560,7 @@ async def test_if_not_fires_if_entity_not_match(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.another_entity", - "below": 100, + "below": below, }, "action": {"service": "test.automation"}, } @@ -507,7 +572,8 @@ async def test_if_not_fires_if_entity_not_match(hass, calls): assert len(calls) == 0 -async def test_if_fires_on_entity_change_below_with_attribute(hass, calls): +@pytest.mark.parametrize("below", (10, "input_number.value_10")) +async def test_if_fires_on_entity_change_below_with_attribute(hass, calls, below): """Test attributes change.""" hass.states.async_set("test.entity", 11, {"test_attribute": 11}) await hass.async_block_till_done() @@ -520,7 +586,7 @@ async def test_if_fires_on_entity_change_below_with_attribute(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "below": 10, + "below": below, }, "action": {"service": "test.automation"}, } @@ -532,7 +598,10 @@ async def test_if_fires_on_entity_change_below_with_attribute(hass, calls): assert len(calls) == 1 -async def test_if_not_fires_on_entity_change_not_below_with_attribute(hass, calls): +@pytest.mark.parametrize("below", (10, "input_number.value_10")) +async def test_if_not_fires_on_entity_change_not_below_with_attribute( + hass, calls, below +): """Test attributes.""" assert await async_setup_component( hass, @@ -542,7 +611,7 @@ async def test_if_not_fires_on_entity_change_not_below_with_attribute(hass, call "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "below": 10, + "below": below, }, "action": {"service": "test.automation"}, } @@ -554,7 +623,8 @@ async def test_if_not_fires_on_entity_change_not_below_with_attribute(hass, call assert len(calls) == 0 -async def test_if_fires_on_attribute_change_with_attribute_below(hass, calls): +@pytest.mark.parametrize("below", (10, "input_number.value_10")) +async def test_if_fires_on_attribute_change_with_attribute_below(hass, calls, below): """Test attributes change.""" hass.states.async_set("test.entity", "entity", {"test_attribute": 11}) await hass.async_block_till_done() @@ -568,7 +638,7 @@ async def test_if_fires_on_attribute_change_with_attribute_below(hass, calls): "platform": "numeric_state", "entity_id": "test.entity", "value_template": "{{ state.attributes.test_attribute }}", - "below": 10, + "below": below, }, "action": {"service": "test.automation"}, } @@ -580,7 +650,10 @@ async def test_if_fires_on_attribute_change_with_attribute_below(hass, calls): assert len(calls) == 1 -async def test_if_not_fires_on_attribute_change_with_attribute_not_below(hass, calls): +@pytest.mark.parametrize("below", (10, "input_number.value_10")) +async def test_if_not_fires_on_attribute_change_with_attribute_not_below( + hass, calls, below +): """Test attributes change.""" assert await async_setup_component( hass, @@ -591,7 +664,7 @@ async def test_if_not_fires_on_attribute_change_with_attribute_not_below(hass, c "platform": "numeric_state", "entity_id": "test.entity", "value_template": "{{ state.attributes.test_attribute }}", - "below": 10, + "below": below, }, "action": {"service": "test.automation"}, } @@ -603,7 +676,8 @@ async def test_if_not_fires_on_attribute_change_with_attribute_not_below(hass, c assert len(calls) == 0 -async def test_if_not_fires_on_entity_change_with_attribute_below(hass, calls): +@pytest.mark.parametrize("below", (10, "input_number.value_10")) +async def test_if_not_fires_on_entity_change_with_attribute_below(hass, calls, below): """Test attributes change.""" assert await async_setup_component( hass, @@ -614,7 +688,7 @@ async def test_if_not_fires_on_entity_change_with_attribute_below(hass, calls): "platform": "numeric_state", "entity_id": "test.entity", "value_template": "{{ state.attributes.test_attribute }}", - "below": 10, + "below": below, }, "action": {"service": "test.automation"}, } @@ -626,7 +700,10 @@ async def test_if_not_fires_on_entity_change_with_attribute_below(hass, calls): assert len(calls) == 0 -async def test_if_not_fires_on_entity_change_with_not_attribute_below(hass, calls): +@pytest.mark.parametrize("below", (10, "input_number.value_10")) +async def test_if_not_fires_on_entity_change_with_not_attribute_below( + hass, calls, below +): """Test attributes change.""" assert await async_setup_component( hass, @@ -637,7 +714,7 @@ async def test_if_not_fires_on_entity_change_with_not_attribute_below(hass, call "platform": "numeric_state", "entity_id": "test.entity", "value_template": "{{ state.attributes.test_attribute }}", - "below": 10, + "below": below, }, "action": {"service": "test.automation"}, } @@ -649,7 +726,10 @@ async def test_if_not_fires_on_entity_change_with_not_attribute_below(hass, call assert len(calls) == 0 -async def test_fires_on_attr_change_with_attribute_below_and_multiple_attr(hass, calls): +@pytest.mark.parametrize("below", (10, "input_number.value_10")) +async def test_fires_on_attr_change_with_attribute_below_and_multiple_attr( + hass, calls, below +): """Test attributes change.""" hass.states.async_set( "test.entity", "entity", {"test_attribute": 11, "not_test_attribute": 11} @@ -664,7 +744,7 @@ async def test_fires_on_attr_change_with_attribute_below_and_multiple_attr(hass, "platform": "numeric_state", "entity_id": "test.entity", "value_template": "{{ state.attributes.test_attribute }}", - "below": 10, + "below": below, }, "action": {"service": "test.automation"}, } @@ -678,7 +758,8 @@ async def test_fires_on_attr_change_with_attribute_below_and_multiple_attr(hass, assert len(calls) == 1 -async def test_template_list(hass, calls): +@pytest.mark.parametrize("below", (10, "input_number.value_10")) +async def test_template_list(hass, calls, below): """Test template list.""" hass.states.async_set("test.entity", "entity", {"test_attribute": [11, 15, 11]}) await hass.async_block_till_done() @@ -691,7 +772,7 @@ async def test_template_list(hass, calls): "platform": "numeric_state", "entity_id": "test.entity", "value_template": "{{ state.attributes.test_attribute[2] }}", - "below": 10, + "below": below, }, "action": {"service": "test.automation"}, } @@ -703,7 +784,8 @@ async def test_template_list(hass, calls): assert len(calls) == 1 -async def test_template_string(hass, calls): +@pytest.mark.parametrize("below", (10.0, "input_number.value_10")) +async def test_template_string(hass, calls, below): """Test template string.""" assert await async_setup_component( hass, @@ -714,7 +796,7 @@ async def test_template_string(hass, calls): "platform": "numeric_state", "entity_id": "test.entity", "value_template": "{{ state.attributes.test_attribute | multiply(10) }}", - "below": 10, + "below": below, }, "action": { "service": "test.automation", @@ -742,7 +824,7 @@ async def test_template_string(hass, calls): assert len(calls) == 1 assert ( calls[0].data["some"] - == "numeric_state - test.entity - 10.0 - None - test state 1 - test state 2" + == f"numeric_state - test.entity - {below} - None - test state 1 - test state 2" ) @@ -771,7 +853,16 @@ async def test_not_fires_on_attr_change_with_attr_not_below_multiple_attr(hass, assert len(calls) == 0 -async def test_if_action(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (8, 12), + (8, "input_number.value_12"), + ("input_number.value_8", 12), + ("input_number.value_8", "input_number.value_12"), + ), +) +async def test_if_action(hass, calls, above, below): """Test if action.""" entity_id = "domain.test_entity" assert await async_setup_component( @@ -783,8 +874,8 @@ async def test_if_action(hass, calls): "condition": { "condition": "numeric_state", "entity_id": entity_id, - "above": 8, - "below": 12, + "above": above, + "below": below, }, "action": {"service": "test.automation"}, } @@ -810,7 +901,16 @@ async def test_if_action(hass, calls): assert len(calls) == 2 -async def test_if_fails_setup_bad_for(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (8, 12), + (8, "input_number.value_12"), + ("input_number.value_8", 12), + ("input_number.value_8", "input_number.value_12"), + ), +) +async def test_if_fails_setup_bad_for(hass, calls, above, below): """Test for setup failure for bad for.""" hass.states.async_set("test.entity", 5) await hass.async_block_till_done() @@ -823,8 +923,8 @@ async def test_if_fails_setup_bad_for(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 8, - "below": 12, + "above": above, + "below": below, "for": {"invalid": 5}, }, "action": {"service": "homeassistant.turn_on"}, @@ -857,7 +957,16 @@ async def test_if_fails_setup_for_without_above_below(hass, calls): ) -async def test_if_not_fires_on_entity_change_with_for(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (8, 12), + (8, "input_number.value_12"), + ("input_number.value_8", 12), + ("input_number.value_8", "input_number.value_12"), + ), +) +async def test_if_not_fires_on_entity_change_with_for(hass, calls, above, below): """Test for not firing on entity change with for.""" assert await async_setup_component( hass, @@ -867,8 +976,8 @@ async def test_if_not_fires_on_entity_change_with_for(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 8, - "below": 12, + "above": above, + "below": below, "for": {"seconds": 5}, }, "action": {"service": "test.automation"}, @@ -885,7 +994,18 @@ async def test_if_not_fires_on_entity_change_with_for(hass, calls): assert len(calls) == 0 -async def test_if_not_fires_on_entities_change_with_for_after_stop(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (8, 12), + (8, "input_number.value_12"), + ("input_number.value_8", 12), + ("input_number.value_8", "input_number.value_12"), + ), +) +async def test_if_not_fires_on_entities_change_with_for_after_stop( + hass, calls, above, below +): """Test for not firing on entities change with for after stop.""" hass.states.async_set("test.entity_1", 0) hass.states.async_set("test.entity_2", 0) @@ -899,8 +1019,8 @@ async def test_if_not_fires_on_entities_change_with_for_after_stop(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": ["test.entity_1", "test.entity_2"], - "above": 8, - "below": 12, + "above": above, + "below": below, "for": {"seconds": 5}, }, "action": {"service": "test.automation"}, @@ -932,7 +1052,18 @@ async def test_if_not_fires_on_entities_change_with_for_after_stop(hass, calls): assert len(calls) == 1 -async def test_if_fires_on_entity_change_with_for_attribute_change(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (8, 12), + (8, "input_number.value_12"), + ("input_number.value_8", 12), + ("input_number.value_8", "input_number.value_12"), + ), +) +async def test_if_fires_on_entity_change_with_for_attribute_change( + hass, calls, above, below +): """Test for firing on entity change with for and attribute change.""" hass.states.async_set("test.entity", 0) await hass.async_block_till_done() @@ -945,8 +1076,8 @@ async def test_if_fires_on_entity_change_with_for_attribute_change(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 8, - "below": 12, + "above": above, + "below": below, "for": {"seconds": 5}, }, "action": {"service": "test.automation"}, @@ -970,7 +1101,16 @@ async def test_if_fires_on_entity_change_with_for_attribute_change(hass, calls): assert len(calls) == 1 -async def test_if_fires_on_entity_change_with_for(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (8, 12), + (8, "input_number.value_12"), + ("input_number.value_8", 12), + ("input_number.value_8", "input_number.value_12"), + ), +) +async def test_if_fires_on_entity_change_with_for(hass, calls, above, below): """Test for firing on entity change with for.""" hass.states.async_set("test.entity", 0) await hass.async_block_till_done() @@ -983,8 +1123,8 @@ async def test_if_fires_on_entity_change_with_for(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 8, - "below": 12, + "above": above, + "below": below, "for": {"seconds": 5}, }, "action": {"service": "test.automation"}, @@ -999,7 +1139,8 @@ async def test_if_fires_on_entity_change_with_for(hass, calls): assert len(calls) == 1 -async def test_wait_template_with_trigger(hass, calls): +@pytest.mark.parametrize("above", (10, "input_number.value_10")) +async def test_wait_template_with_trigger(hass, calls, above): """Test using wait template with 'trigger.entity_id'.""" hass.states.async_set("test.entity", "0") await hass.async_block_till_done() @@ -1012,7 +1153,7 @@ async def test_wait_template_with_trigger(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 10, + "above": above, }, "action": [ {"wait_template": "{{ states(trigger.entity_id) | int < 10 }}"}, @@ -1039,7 +1180,16 @@ async def test_wait_template_with_trigger(hass, calls): assert "numeric_state - test.entity - 12" == calls[0].data["some"] -async def test_if_fires_on_entities_change_no_overlap(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (8, 12), + (8, "input_number.value_12"), + ("input_number.value_8", 12), + ("input_number.value_8", "input_number.value_12"), + ), +) +async def test_if_fires_on_entities_change_no_overlap(hass, calls, above, below): """Test for firing on entities change with no overlap.""" hass.states.async_set("test.entity_1", 0) hass.states.async_set("test.entity_2", 0) @@ -1053,8 +1203,8 @@ async def test_if_fires_on_entities_change_no_overlap(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": ["test.entity_1", "test.entity_2"], - "above": 8, - "below": 12, + "above": above, + "below": below, "for": {"seconds": 5}, }, "action": { @@ -1086,7 +1236,16 @@ async def test_if_fires_on_entities_change_no_overlap(hass, calls): assert calls[1].data["some"] == "test.entity_2" -async def test_if_fires_on_entities_change_overlap(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (8, 12), + (8, "input_number.value_12"), + ("input_number.value_8", 12), + ("input_number.value_8", "input_number.value_12"), + ), +) +async def test_if_fires_on_entities_change_overlap(hass, calls, above, below): """Test for firing on entities change with overlap.""" hass.states.async_set("test.entity_1", 0) hass.states.async_set("test.entity_2", 0) @@ -1100,8 +1259,8 @@ async def test_if_fires_on_entities_change_overlap(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": ["test.entity_1", "test.entity_2"], - "above": 8, - "below": 12, + "above": above, + "below": below, "for": {"seconds": 5}, }, "action": { @@ -1144,7 +1303,16 @@ async def test_if_fires_on_entities_change_overlap(hass, calls): assert calls[1].data["some"] == "test.entity_2" -async def test_if_fires_on_change_with_for_template_1(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (8, 12), + (8, "input_number.value_12"), + ("input_number.value_8", 12), + ("input_number.value_8", "input_number.value_12"), + ), +) +async def test_if_fires_on_change_with_for_template_1(hass, calls, above, below): """Test for firing on change with for template.""" hass.states.async_set("test.entity", 0) await hass.async_block_till_done() @@ -1157,8 +1325,8 @@ async def test_if_fires_on_change_with_for_template_1(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 8, - "below": 12, + "above": above, + "below": below, "for": {"seconds": "{{ 5 }}"}, }, "action": {"service": "test.automation"}, @@ -1174,7 +1342,16 @@ async def test_if_fires_on_change_with_for_template_1(hass, calls): assert len(calls) == 1 -async def test_if_fires_on_change_with_for_template_2(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (8, 12), + (8, "input_number.value_12"), + ("input_number.value_8", 12), + ("input_number.value_8", "input_number.value_12"), + ), +) +async def test_if_fires_on_change_with_for_template_2(hass, calls, above, below): """Test for firing on change with for template.""" hass.states.async_set("test.entity", 0) await hass.async_block_till_done() @@ -1187,8 +1364,8 @@ async def test_if_fires_on_change_with_for_template_2(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 8, - "below": 12, + "above": above, + "below": below, "for": "{{ 5 }}", }, "action": {"service": "test.automation"}, @@ -1204,7 +1381,16 @@ async def test_if_fires_on_change_with_for_template_2(hass, calls): assert len(calls) == 1 -async def test_if_fires_on_change_with_for_template_3(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (8, 12), + (8, "input_number.value_12"), + ("input_number.value_8", 12), + ("input_number.value_8", "input_number.value_12"), + ), +) +async def test_if_fires_on_change_with_for_template_3(hass, calls, above, below): """Test for firing on change with for template.""" hass.states.async_set("test.entity", 0) await hass.async_block_till_done() @@ -1217,8 +1403,8 @@ async def test_if_fires_on_change_with_for_template_3(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 8, - "below": 12, + "above": above, + "below": below, "for": "00:00:{{ 5 }}", }, "action": {"service": "test.automation"}, @@ -1234,7 +1420,16 @@ async def test_if_fires_on_change_with_for_template_3(hass, calls): assert len(calls) == 1 -async def test_invalid_for_template(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (8, 12), + (8, "input_number.value_12"), + ("input_number.value_8", 12), + ("input_number.value_8", "input_number.value_12"), + ), +) +async def test_invalid_for_template(hass, calls, above, below): """Test for invalid for template.""" hass.states.async_set("test.entity", 0) await hass.async_block_till_done() @@ -1247,8 +1442,8 @@ async def test_invalid_for_template(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 8, - "below": 12, + "above": above, + "below": below, "for": "{{ five }}", }, "action": {"service": "test.automation"}, @@ -1262,7 +1457,18 @@ async def test_invalid_for_template(hass, calls): assert mock_logger.error.called -async def test_if_fires_on_entities_change_overlap_for_template(hass, calls): +@pytest.mark.parametrize( + "above, below", + ( + (8, 12), + (8, "input_number.value_12"), + ("input_number.value_8", 12), + ("input_number.value_8", "input_number.value_12"), + ), +) +async def test_if_fires_on_entities_change_overlap_for_template( + hass, calls, above, below +): """Test for firing on entities change with overlap and for template.""" hass.states.async_set("test.entity_1", 0) hass.states.async_set("test.entity_2", 0) @@ -1276,8 +1482,8 @@ async def test_if_fires_on_entities_change_overlap_for_template(hass, calls): "trigger": { "platform": "numeric_state", "entity_id": ["test.entity_1", "test.entity_2"], - "above": 8, - "below": 12, + "above": above, + "below": below, "for": '{{ 5 if trigger.entity_id == "test.entity_1"' " else 10 }}", }, @@ -1335,7 +1541,30 @@ def test_below_above(): ) -async def test_attribute_if_fires_on_entity_change_with_both_filters(hass, calls): +def test_schema_input_number(): + """Test input_number only is accepted for above/below.""" + with pytest.raises(vol.Invalid): + numeric_state_trigger.TRIGGER_SCHEMA( + { + "platform": "numeric_state", + "above": "input_datetime.some_input", + "below": 1000, + } + ) + with pytest.raises(vol.Invalid): + numeric_state_trigger.TRIGGER_SCHEMA( + { + "platform": "numeric_state", + "below": "input_datetime.some_input", + "above": 1200, + } + ) + + +@pytest.mark.parametrize("above", (3, "input_number.value_3")) +async def test_attribute_if_fires_on_entity_change_with_both_filters( + hass, calls, above +): """Test for firing if both filters are match attribute.""" hass.states.async_set("test.entity", "bla", {"test-measurement": 1}) @@ -1347,7 +1576,7 @@ async def test_attribute_if_fires_on_entity_change_with_both_filters(hass, calls "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 3, + "above": above, "attribute": "test-measurement", }, "action": {"service": "test.automation"}, @@ -1361,8 +1590,9 @@ async def test_attribute_if_fires_on_entity_change_with_both_filters(hass, calls assert len(calls) == 1 +@pytest.mark.parametrize("above", (3, "input_number.value_3")) async def test_attribute_if_not_fires_on_entities_change_with_for_after_stop( - hass, calls + hass, calls, above ): """Test for not firing on entity change with for after stop trigger.""" hass.states.async_set("test.entity", "bla", {"test-measurement": 1}) @@ -1375,7 +1605,7 @@ async def test_attribute_if_not_fires_on_entities_change_with_for_after_stop( "trigger": { "platform": "numeric_state", "entity_id": "test.entity", - "above": 3, + "above": above, "attribute": "test-measurement", "for": 5, }, diff --git a/tests/components/homeassistant/triggers/test_state.py b/tests/components/homeassistant/triggers/test_state.py index 61fa991e0f4..dd98dbc429c 100644 --- a/tests/components/homeassistant/triggers/test_state.py +++ b/tests/components/homeassistant/triggers/test_state.py @@ -1,5 +1,6 @@ """The test for state automation.""" from datetime import timedelta +from unittest.mock import patch import pytest @@ -10,7 +11,6 @@ from homeassistant.core import Context from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import ( assert_setup_component, async_fire_time_changed, diff --git a/tests/components/homeassistant/triggers/test_time.py b/tests/components/homeassistant/triggers/test_time.py index a37be71102d..5805aa07fe3 100644 --- a/tests/components/homeassistant/triggers/test_time.py +++ b/tests/components/homeassistant/triggers/test_time.py @@ -1,5 +1,6 @@ """The tests for the time automation.""" from datetime import timedelta +from unittest.mock import Mock, patch import pytest import voluptuous as vol @@ -10,7 +11,6 @@ from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, SERVICE_TURN_ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import Mock, patch from tests.common import ( assert_setup_component, async_fire_time_changed, diff --git a/tests/components/homeassistant/triggers/test_time_pattern.py b/tests/components/homeassistant/triggers/test_time_pattern.py index f428bcf29bc..147bb388fed 100644 --- a/tests/components/homeassistant/triggers/test_time_pattern.py +++ b/tests/components/homeassistant/triggers/test_time_pattern.py @@ -1,5 +1,6 @@ """The tests for the time_pattern automation.""" from datetime import timedelta +from unittest.mock import patch import pytest import voluptuous as vol @@ -10,7 +11,6 @@ from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_O from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import async_fire_time_changed, async_mock_service, mock_component diff --git a/tests/components/homekit/common.py b/tests/components/homekit/common.py index 4ad5f60551d..20aa0e04c2b 100644 --- a/tests/components/homekit/common.py +++ b/tests/components/homekit/common.py @@ -1,5 +1,5 @@ """Collection of fixtures and functions for the HomeKit tests.""" -from tests.async_mock import Mock, patch +from unittest.mock import Mock, patch EMPTY_8_6_JPEG = b"empty_8_6" diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py index 0cb31e1b701..ac51c4e6368 100644 --- a/tests/components/homekit/conftest.py +++ b/tests/components/homekit/conftest.py @@ -1,12 +1,12 @@ """HomeKit session fixtures.""" +from unittest.mock import patch + from pyhap.accessory_driver import AccessoryDriver import pytest from homeassistant.components.homekit.const import EVENT_HOMEKIT_CHANGED from homeassistant.core import callback as ha_callback -from tests.async_mock import patch - @pytest.fixture def hk_driver(loop): diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index 91f02522126..886123062c4 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -3,6 +3,7 @@ This includes tests for all mock object types. """ from datetime import timedelta +from unittest.mock import Mock, patch import pytest @@ -46,7 +47,6 @@ from homeassistant.const import ( from homeassistant.helpers.event import TRACK_STATE_CHANGE_CALLBACKS import homeassistant.util.dt as dt_util -from tests.async_mock import Mock, patch from tests.common import async_fire_time_changed, async_mock_service diff --git a/tests/components/homekit/test_aidmanager.py b/tests/components/homekit/test_aidmanager.py index b7c12e86443..df1bb14dd9e 100644 --- a/tests/components/homekit/test_aidmanager.py +++ b/tests/components/homekit/test_aidmanager.py @@ -1,5 +1,6 @@ """Tests for the HomeKit AID manager.""" import os +from unittest.mock import patch from fnvhash import fnv1a_32 import pytest @@ -12,7 +13,6 @@ from homeassistant.components.homekit.aidmanager import ( from homeassistant.helpers import device_registry from homeassistant.helpers.storage import STORAGE_DIR -from tests.async_mock import patch from tests.common import MockConfigEntry, mock_device_registry, mock_registry diff --git a/tests/components/homekit/test_config_flow.py b/tests/components/homekit/test_config_flow.py index 59d65977066..4438404af2e 100644 --- a/tests/components/homekit/test_config_flow.py +++ b/tests/components/homekit/test_config_flow.py @@ -1,4 +1,6 @@ """Test the HomeKit config flow.""" +from unittest.mock import patch + import pytest from homeassistant import config_entries, data_entry_flow, setup @@ -6,7 +8,6 @@ from homeassistant.components.homekit.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_NAME, CONF_PORT -from tests.async_mock import patch from tests.common import MockConfigEntry @@ -27,7 +28,6 @@ def _mock_config_entry_with_options_populated(): ], "exclude_entities": ["climate.front_gate"], }, - "safe_mode": False, }, ) @@ -158,7 +158,7 @@ async def test_options_flow_exclude_mode_advanced(auto_start, hass): with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): result3 = await hass.config_entries.options.async_configure( result2["flow_id"], - user_input={"auto_start": auto_start, "safe_mode": True}, + user_input={"auto_start": auto_start}, ) assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -171,7 +171,6 @@ async def test_options_flow_exclude_mode_advanced(auto_start, hass): "include_domains": ["fan", "vacuum", "climate", "humidifier"], "include_entities": [], }, - "safe_mode": True, } @@ -203,16 +202,7 @@ async def test_options_flow_exclude_mode_basic(hass): result["flow_id"], user_input={"entities": ["climate.old"], "include_exclude_mode": "exclude"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["step_id"] == "advanced" - - with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): - result3 = await hass.config_entries.options.async_configure( - result2["flow_id"], - user_input={"safe_mode": True}, - ) - - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { "auto_start": True, "mode": "bridge", @@ -222,7 +212,6 @@ async def test_options_flow_exclude_mode_basic(hass): "include_domains": ["fan", "vacuum", "climate"], "include_entities": [], }, - "safe_mode": True, } @@ -256,16 +245,7 @@ async def test_options_flow_include_mode_basic(hass): result["flow_id"], user_input={"entities": ["climate.new"], "include_exclude_mode": "include"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["step_id"] == "advanced" - - with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): - result3 = await hass.config_entries.options.async_configure( - result2["flow_id"], - user_input={"safe_mode": True}, - ) - - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { "auto_start": True, "mode": "bridge", @@ -275,7 +255,6 @@ async def test_options_flow_include_mode_basic(hass): "include_domains": ["fan", "vacuum"], "include_entities": ["climate.new"], }, - "safe_mode": True, } @@ -322,16 +301,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass): user_input={"camera_copy": ["camera.native_h264"]}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result3["step_id"] == "advanced" - - with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): - result4 = await hass.config_entries.options.async_configure( - result3["flow_id"], - user_input={"safe_mode": True}, - ) - - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { "auto_start": True, "mode": "bridge", @@ -342,7 +312,6 @@ async def test_options_flow_exclude_mode_with_cameras(hass): "include_entities": [], }, "entity_config": {"camera.native_h264": {"video_codec": "copy"}}, - "safe_mode": True, } # Now run though again and verify we can turn off copy @@ -377,16 +346,7 @@ async def test_options_flow_exclude_mode_with_cameras(hass): user_input={"camera_copy": []}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result3["step_id"] == "advanced" - - with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): - result4 = await hass.config_entries.options.async_configure( - result3["flow_id"], - user_input={"safe_mode": True}, - ) - - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { "auto_start": True, "mode": "bridge", @@ -397,7 +357,6 @@ async def test_options_flow_exclude_mode_with_cameras(hass): "include_entities": [], }, "entity_config": {"camera.native_h264": {}}, - "safe_mode": True, } @@ -444,16 +403,7 @@ async def test_options_flow_include_mode_with_cameras(hass): user_input={"camera_copy": ["camera.native_h264"]}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result3["step_id"] == "advanced" - - with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): - result4 = await hass.config_entries.options.async_configure( - result3["flow_id"], - user_input={"safe_mode": True}, - ) - - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { "auto_start": True, "mode": "bridge", @@ -464,7 +414,6 @@ async def test_options_flow_include_mode_with_cameras(hass): "include_entities": ["camera.native_h264", "camera.transcode_h264"], }, "entity_config": {"camera.native_h264": {"video_codec": "copy"}}, - "safe_mode": True, } # Now run though again and verify we can turn off copy @@ -499,16 +448,7 @@ async def test_options_flow_include_mode_with_cameras(hass): user_input={"camera_copy": []}, ) - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result3["step_id"] == "advanced" - - with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): - result4 = await hass.config_entries.options.async_configure( - result3["flow_id"], - user_input={"safe_mode": True}, - ) - - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { "auto_start": True, "mode": "bridge", @@ -519,7 +459,6 @@ async def test_options_flow_include_mode_with_cameras(hass): "include_entities": [], }, "entity_config": {"camera.native_h264": {}}, - "safe_mode": True, } @@ -542,7 +481,6 @@ async def test_options_flow_blocked_when_from_yaml(hass): ], "exclude_entities": ["climate.front_gate"], }, - "safe_mode": False, }, source=SOURCE_IMPORT, ) @@ -593,16 +531,7 @@ async def test_options_flow_include_mode_basic_accessory(hass): result["flow_id"], user_input={"entities": "media_player.tv"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["step_id"] == "advanced" - - with patch("homeassistant.components.homekit.async_setup_entry", return_value=True): - result3 = await hass.config_entries.options.async_configure( - result2["flow_id"], - user_input={"safe_mode": False}, - ) - - assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert config_entry.options == { "auto_start": True, "mode": "accessory", @@ -612,5 +541,4 @@ async def test_options_flow_include_mode_basic_accessory(hass): "include_domains": [], "include_entities": ["media_player.tv"], }, - "safe_mode": False, } diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index ea91733fdab..70d59408011 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -1,4 +1,6 @@ """Package to test the get_accessory method.""" +from unittest.mock import Mock, patch + import pytest import homeassistant.components.climate as climate @@ -31,8 +33,6 @@ from homeassistant.const import ( ) from homeassistant.core import State -from tests.async_mock import Mock, patch - def test_not_supported(caplog): """Test if none is returned if entity isn't supported.""" @@ -257,7 +257,7 @@ def test_type_switches(type_name, entity_id, state, attrs, config): "type_name, entity_id, state, attrs", [ ( - "DockVacuum", + "Vacuum", "vacuum.dock_vacuum", "docked", { @@ -265,7 +265,7 @@ def test_type_switches(type_name, entity_id, state, attrs, config): | vacuum.SUPPORT_RETURN_HOME }, ), - ("Switch", "vacuum.basic_vacuum", "off", {}), + ("Vacuum", "vacuum.basic_vacuum", "off", {}), ], ) def test_type_vacuum(type_name, entity_id, state, attrs): diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index 8b79c3f6c58..c6f897c32a2 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -1,8 +1,10 @@ """Tests for the HomeKit component.""" import os from typing import Dict +from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch from pyhap.accessory import Accessory +from pyhap.const import CATEGORY_CAMERA, CATEGORY_TELEVISION import pytest from homeassistant import config as hass_config @@ -26,9 +28,7 @@ from homeassistant.components.homekit.const import ( BRIDGE_SERIAL_NUMBER, CONF_AUTO_START, CONF_ENTRY_INDEX, - CONF_SAFE_MODE, DEFAULT_PORT, - DEFAULT_SAFE_MODE, DOMAIN, HOMEKIT, HOMEKIT_FILE, @@ -66,7 +66,6 @@ from homeassistant.util import json as json_util from .util import PATH_HOMEKIT, async_init_entry, async_init_integration -from tests.async_mock import ANY, AsyncMock, MagicMock, Mock, patch from tests.common import MockConfigEntry, mock_device_registry, mock_registry from tests.components.homekit.common import patch_debounce @@ -98,7 +97,7 @@ def debounce_patcher_fixture(): patcher.stop() -async def test_setup_min(hass): +async def test_setup_min(hass, mock_zeroconf): """Test async_setup with min config options.""" entry = MockConfigEntry( domain=DOMAIN, @@ -120,7 +119,6 @@ async def test_setup_min(hass): None, ANY, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, None, entry.entry_id, @@ -135,12 +133,12 @@ async def test_setup_min(hass): mock_homekit().async_start.assert_called() -async def test_setup_auto_start_disabled(hass): +async def test_setup_auto_start_disabled(hass, mock_zeroconf): """Test async_setup with auto start disabled and test service calls.""" entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "Test Name", CONF_PORT: 11111, CONF_IP_ADDRESS: "172.0.0.0"}, - options={CONF_AUTO_START: False, CONF_SAFE_MODE: DEFAULT_SAFE_MODE}, + options={CONF_AUTO_START: False}, ) entry.add_to_hass(hass) @@ -157,7 +155,6 @@ async def test_setup_auto_start_disabled(hass): "172.0.0.0", ANY, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, None, entry.entry_id, @@ -190,7 +187,7 @@ async def test_setup_auto_start_disabled(hass): assert homekit.async_start.called is False -async def test_homekit_setup(hass, hk_driver): +async def test_homekit_setup(hass, hk_driver, mock_zeroconf): """Test setup of bridge and driver.""" entry = MockConfigEntry( domain=DOMAIN, @@ -204,7 +201,6 @@ async def test_homekit_setup(hass, hk_driver): None, {}, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -237,7 +233,7 @@ async def test_homekit_setup(hass, hk_driver): assert hass.bus.async_listeners().get(EVENT_HOMEASSISTANT_STOP) == 1 -async def test_homekit_setup_ip_address(hass, hk_driver): +async def test_homekit_setup_ip_address(hass, hk_driver, mock_zeroconf): """Test setup with given IP address.""" entry = MockConfigEntry( domain=DOMAIN, @@ -251,7 +247,6 @@ async def test_homekit_setup_ip_address(hass, hk_driver): "172.0.0.0", {}, {}, - None, HOMEKIT_MODE_BRIDGE, None, entry_id=entry.entry_id, @@ -276,7 +271,7 @@ async def test_homekit_setup_ip_address(hass, hk_driver): ) -async def test_homekit_setup_advertise_ip(hass, hk_driver): +async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_zeroconf): """Test setup with given IP address to advertise.""" entry = MockConfigEntry( domain=DOMAIN, @@ -290,7 +285,6 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver): "0.0.0.0", {}, {}, - None, HOMEKIT_MODE_BRIDGE, "192.168.1.100", entry_id=entry.entry_id, @@ -315,32 +309,7 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver): ) -async def test_homekit_setup_safe_mode(hass, hk_driver): - """Test if safe_mode flag is set.""" - entry = MockConfigEntry( - domain=DOMAIN, - data={CONF_NAME: "mock_name", CONF_PORT: 12345}, - source=SOURCE_IMPORT, - ) - homekit = HomeKit( - hass, - BRIDGE_NAME, - DEFAULT_PORT, - None, - {}, - {}, - True, - HOMEKIT_MODE_BRIDGE, - advertise_ip=None, - entry_id=entry.entry_id, - ) - - with patch(f"{PATH_HOMEKIT}.accessories.HomeDriver", return_value=hk_driver): - await hass.async_add_executor_job(homekit.setup, MagicMock()) - assert homekit.driver.safe_mode is True - - -async def test_homekit_add_accessory(hass): +async def test_homekit_add_accessory(hass, mock_zeroconf): """Add accessory if config exists and get_acc returns an accessory.""" entry = await async_init_integration(hass) @@ -351,7 +320,6 @@ async def test_homekit_add_accessory(hass): None, lambda entity_id: True, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -360,10 +328,12 @@ async def test_homekit_add_accessory(hass): homekit.bridge = mock_bridge = Mock() homekit.bridge.accessories = range(10) + mock_acc = Mock(category="any") + await async_init_integration(hass) with patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc: - mock_get_acc.side_effect = [None, "acc", None] + mock_get_acc.side_effect = [None, mock_acc, None] homekit.add_bridge_accessory(State("light.demo", "on")) mock_get_acc.assert_called_with(hass, "driver", ANY, 1403373688, {}) assert not mock_bridge.add_accessory.called @@ -374,10 +344,49 @@ async def test_homekit_add_accessory(hass): homekit.add_bridge_accessory(State("demo.test_2", "on")) mock_get_acc.assert_called_with(hass, "driver", ANY, 1467253281, {}) - mock_bridge.add_accessory.assert_called_with("acc") + mock_bridge.add_accessory.assert_called_with(mock_acc) -async def test_homekit_remove_accessory(hass): +@pytest.mark.parametrize("acc_category", [CATEGORY_TELEVISION, CATEGORY_CAMERA]) +async def test_homekit_warn_add_accessory_bridge( + hass, acc_category, mock_zeroconf, caplog +): + """Test we warn when adding cameras or tvs to a bridge.""" + entry = await async_init_integration(hass) + + homekit = HomeKit( + hass, + None, + None, + None, + lambda entity_id: True, + {}, + HOMEKIT_MODE_BRIDGE, + advertise_ip=None, + entry_id=entry.entry_id, + ) + homekit.driver = "driver" + homekit.bridge = mock_bridge = Mock() + homekit.bridge.accessories = range(10) + + mock_camera_acc = Mock(category=acc_category) + + await async_init_integration(hass) + + with patch(f"{PATH_HOMEKIT}.get_accessory") as mock_get_acc: + mock_get_acc.side_effect = [None, mock_camera_acc, None] + homekit.add_bridge_accessory(State("light.demo", "on")) + mock_get_acc.assert_called_with(hass, "driver", ANY, 1403373688, {}) + assert not mock_bridge.add_accessory.called + + homekit.add_bridge_accessory(State("camera.test", "on")) + mock_get_acc.assert_called_with(hass, "driver", ANY, 1508819236, {}) + assert mock_bridge.add_accessory.called + + assert "accessory mode" in caplog.text + + +async def test_homekit_remove_accessory(hass, mock_zeroconf): """Remove accessory from bridge.""" entry = await async_init_integration(hass) @@ -388,7 +397,6 @@ async def test_homekit_remove_accessory(hass): None, lambda entity_id: True, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -402,7 +410,7 @@ async def test_homekit_remove_accessory(hass): assert len(mock_bridge.accessories) == 0 -async def test_homekit_entity_filter(hass): +async def test_homekit_entity_filter(hass, mock_zeroconf): """Test the entity filter.""" entry = await async_init_integration(hass) @@ -414,7 +422,6 @@ async def test_homekit_entity_filter(hass): None, entity_filter, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -437,7 +444,7 @@ async def test_homekit_entity_filter(hass): assert mock_get_acc.called is False -async def test_homekit_entity_glob_filter(hass): +async def test_homekit_entity_glob_filter(hass, mock_zeroconf): """Test the entity filter.""" entry = await async_init_integration(hass) @@ -451,7 +458,6 @@ async def test_homekit_entity_glob_filter(hass): None, entity_filter, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -491,7 +497,6 @@ async def test_homekit_start(hass, hk_driver, device_reg, debounce_patcher): None, {}, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -541,7 +546,7 @@ async def test_homekit_start(hass, hk_driver, device_reg, debounce_patcher): assert device_reg.async_get(bridge_with_wrong_mac.id) is None device = device_reg.async_get_device( - {(DOMAIN, entry.entry_id, BRIDGE_SERIAL_NUMBER)}, {} + {(DOMAIN, entry.entry_id, BRIDGE_SERIAL_NUMBER)} ) assert device formatted_mac = device_registry.format_mac(homekit.driver.state.mac) @@ -559,7 +564,7 @@ async def test_homekit_start(hass, hk_driver, device_reg, debounce_patcher): await homekit.async_start() device = device_reg.async_get_device( - {(DOMAIN, entry.entry_id, BRIDGE_SERIAL_NUMBER)}, {} + {(DOMAIN, entry.entry_id, BRIDGE_SERIAL_NUMBER)} ) assert device formatted_mac = device_registry.format_mac(homekit.driver.state.mac) @@ -568,7 +573,9 @@ async def test_homekit_start(hass, hk_driver, device_reg, debounce_patcher): assert len(device_reg.devices) == 1 -async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, debounce_patcher): +async def test_homekit_start_with_a_broken_accessory( + hass, hk_driver, debounce_patcher, mock_zeroconf +): """Test HomeKit start method.""" pin = b"123-45-678" entry = MockConfigEntry( @@ -584,7 +591,6 @@ async def test_homekit_start_with_a_broken_accessory(hass, hk_driver, debounce_p None, entity_filter, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -631,7 +637,6 @@ async def test_homekit_stop(hass): None, {}, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -659,7 +664,7 @@ async def test_homekit_stop(hass): assert homekit.driver.async_stop.called is True -async def test_homekit_reset_accessories(hass): +async def test_homekit_reset_accessories(hass, mock_zeroconf): """Test adding too many accessories to HomeKit.""" entry = MockConfigEntry( domain=DOMAIN, data={CONF_NAME: "mock_name", CONF_PORT: 12345} @@ -672,7 +677,6 @@ async def test_homekit_reset_accessories(hass): None, {}, {entity_id: {}}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -708,7 +712,7 @@ async def test_homekit_reset_accessories(hass): homekit.status = STATUS_READY -async def test_homekit_too_many_accessories(hass, hk_driver, caplog): +async def test_homekit_too_many_accessories(hass, hk_driver, caplog, mock_zeroconf): """Test adding too many accessories to HomeKit.""" entry = await async_init_integration(hass) @@ -721,7 +725,6 @@ async def test_homekit_too_many_accessories(hass, hk_driver, caplog): None, entity_filter, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -751,7 +754,7 @@ async def test_homekit_too_many_accessories(hass, hk_driver, caplog): async def test_homekit_finds_linked_batteries( - hass, hk_driver, debounce_patcher, device_reg, entity_reg + hass, hk_driver, debounce_patcher, device_reg, entity_reg, mock_zeroconf ): """Test HomeKit start method.""" entry = await async_init_integration(hass) @@ -763,7 +766,6 @@ async def test_homekit_finds_linked_batteries( None, {}, {"light.demo": {}}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -838,7 +840,7 @@ async def test_homekit_finds_linked_batteries( async def test_homekit_async_get_integration_fails( - hass, hk_driver, debounce_patcher, device_reg, entity_reg + hass, hk_driver, debounce_patcher, device_reg, entity_reg, mock_zeroconf ): """Test that we continue if async_get_integration fails.""" entry = await async_init_integration(hass) @@ -850,7 +852,6 @@ async def test_homekit_async_get_integration_fails( None, {}, {"light.demo": {}}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -923,7 +924,7 @@ async def test_homekit_async_get_integration_fails( ) -async def test_setup_imported(hass): +async def test_setup_imported(hass, mock_zeroconf): """Test async_setup with imported config options.""" legacy_persist_file_path = hass.config.path(HOMEKIT_FILE) legacy_aid_storage_path = hass.config.path(STORAGE_DIR, "homekit.aids") @@ -957,7 +958,6 @@ async def test_setup_imported(hass): None, ANY, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, None, entry.entry_id, @@ -987,7 +987,7 @@ async def test_setup_imported(hass): os.unlink(migrated_aid_file_path) -async def test_yaml_updates_update_config_entry_for_name(hass): +async def test_yaml_updates_update_config_entry_for_name(hass, mock_zeroconf): """Test async_setup with imported config.""" entry = MockConfigEntry( domain=DOMAIN, @@ -1012,7 +1012,6 @@ async def test_yaml_updates_update_config_entry_for_name(hass): None, ANY, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, None, entry.entry_id, @@ -1027,7 +1026,7 @@ async def test_yaml_updates_update_config_entry_for_name(hass): mock_homekit().async_start.assert_called() -async def test_raise_config_entry_not_ready(hass): +async def test_raise_config_entry_not_ready(hass, mock_zeroconf): """Test async_setup when the port is not available.""" entry = MockConfigEntry( domain=DOMAIN, @@ -1073,7 +1072,7 @@ def _write_data(path: str, data: Dict) -> None: async def test_homekit_ignored_missing_devices( - hass, hk_driver, debounce_patcher, device_reg, entity_reg + hass, hk_driver, debounce_patcher, device_reg, entity_reg, mock_zeroconf ): """Test HomeKit handles a device in the entity registry but missing from the device registry.""" entry = await async_init_integration(hass) @@ -1085,7 +1084,6 @@ async def test_homekit_ignored_missing_devices( None, {}, {"light.demo": {}}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -1155,7 +1153,7 @@ async def test_homekit_ignored_missing_devices( async def test_homekit_finds_linked_motion_sensors( - hass, hk_driver, debounce_patcher, device_reg, entity_reg + hass, hk_driver, debounce_patcher, device_reg, entity_reg, mock_zeroconf ): """Test HomeKit start method.""" entry = await async_init_integration(hass) @@ -1167,7 +1165,6 @@ async def test_homekit_finds_linked_motion_sensors( None, {}, {"camera.camera_demo": {}}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -1231,7 +1228,7 @@ async def test_homekit_finds_linked_motion_sensors( async def test_homekit_finds_linked_humidity_sensors( - hass, hk_driver, debounce_patcher, device_reg, entity_reg + hass, hk_driver, debounce_patcher, device_reg, entity_reg, mock_zeroconf ): """Test HomeKit start method.""" entry = await async_init_integration(hass) @@ -1243,7 +1240,6 @@ async def test_homekit_finds_linked_humidity_sensors( None, {}, {"humidifier.humidifier": {}}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, advertise_ip=None, entry_id=entry.entry_id, @@ -1308,7 +1304,7 @@ async def test_homekit_finds_linked_humidity_sensors( ) -async def test_reload(hass): +async def test_reload(hass, mock_zeroconf): """Test we can reload from yaml.""" entry = MockConfigEntry( domain=DOMAIN, @@ -1333,7 +1329,6 @@ async def test_reload(hass): None, ANY, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, None, entry.entry_id, @@ -1370,7 +1365,6 @@ async def test_reload(hass): None, ANY, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_BRIDGE, None, entry.entry_id, @@ -1396,7 +1390,6 @@ async def test_homekit_start_in_accessory_mode( None, {}, {}, - DEFAULT_SAFE_MODE, HOMEKIT_MODE_ACCESSORY, advertise_ip=None, entry_id=entry.entry_id, diff --git a/tests/components/homekit/test_img_util.py b/tests/components/homekit/test_img_util.py index 728bb8847ff..45af8e6b1e6 100644 --- a/tests/components/homekit/test_img_util.py +++ b/tests/components/homekit/test_img_util.py @@ -1,4 +1,6 @@ """Test HomeKit img_util module.""" +from unittest.mock import patch + from homeassistant.components.camera import Image from homeassistant.components.homekit.img_util import ( TurboJPEGSingleton, @@ -7,8 +9,6 @@ from homeassistant.components.homekit.img_util import ( from .common import EMPTY_8_6_JPEG, mock_turbo_jpeg -from tests.async_mock import patch - EMPTY_16_12_JPEG = b"empty_16_12" diff --git a/tests/components/homekit/test_init.py b/tests/components/homekit/test_init.py index 72670667fa7..6643ae9ae18 100644 --- a/tests/components/homekit/test_init.py +++ b/tests/components/homekit/test_init.py @@ -1,4 +1,6 @@ """Test HomeKit initialization.""" +from unittest.mock import patch + from homeassistant.components import logbook from homeassistant.components.homekit.const import ( ATTR_DISPLAY_NAME, @@ -9,7 +11,6 @@ from homeassistant.components.homekit.const import ( from homeassistant.const import ATTR_ENTITY_ID, ATTR_SERVICE from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.components.logbook.test_init import MockLazyEventPartialState diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index 6386b1d8e69..804e03a4e6c 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -1,5 +1,6 @@ """Test different accessory types: Camera.""" +from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch from uuid import UUID from pyhap.accessory_driver import AccessoryDriver @@ -34,8 +35,6 @@ from homeassistant.setup import async_setup_component from .common import mock_turbo_jpeg -from tests.async_mock import AsyncMock, MagicMock, PropertyMock, patch - MOCK_START_STREAM_TLV = "ARUCAQEBEDMD1QMXzEaatnKSQ2pxovYCNAEBAAIJAQECAgECAwEAAwsBAgAFAgLQAgMBHgQXAQFjAgQ768/RAwIrAQQEAAAAPwUCYgUDLAEBAwIMAQEBAgEAAwECBAEUAxYBAW4CBCzq28sDAhgABAQAAKBABgENBAEA" MOCK_END_POINTS_TLV = "ARAzA9UDF8xGmrZykkNqcaL2AgEAAxoBAQACDTE5Mi4xNjguMjA4LjUDAi7IBAKkxwQlAQEAAhDN0+Y0tZ4jzoO0ske9UsjpAw6D76oVXnoi7DbawIG4CwUlAQEAAhCyGcROB8P7vFRDzNF2xrK1Aw6NdcLugju9yCfkWVSaVAYEDoAsAAcEpxV8AA==" MOCK_START_STREAM_SESSION_UUID = UUID("3303d503-17cc-469a-b672-92436a71a2f6") diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index a25567b7004..bc1bac11844 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -1,5 +1,6 @@ """Test different accessory types: Fans.""" from collections import namedtuple +from unittest.mock import Mock from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE import pytest @@ -32,7 +33,6 @@ from homeassistant.const import ( from homeassistant.core import CoreState from homeassistant.helpers import entity_registry -from tests.async_mock import Mock from tests.common import async_mock_service from tests.components.homekit.common import patch_debounce diff --git a/tests/components/homekit/test_type_switches.py b/tests/components/homekit/test_type_switches.py index 4f36adc99e1..5d218a6ef8a 100644 --- a/tests/components/homekit/test_type_switches.py +++ b/tests/components/homekit/test_type_switches.py @@ -10,20 +10,25 @@ from homeassistant.components.homekit.const import ( TYPE_SPRINKLER, TYPE_VALVE, ) -from homeassistant.components.homekit.type_switches import ( - DockVacuum, - Outlet, - Switch, - Valve, -) +from homeassistant.components.homekit.type_switches import Outlet, Switch, Vacuum, Valve from homeassistant.components.vacuum import ( DOMAIN as VACUUM_DOMAIN, SERVICE_RETURN_TO_BASE, SERVICE_START, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, STATE_CLEANING, STATE_DOCKED, + SUPPORT_RETURN_HOME, + SUPPORT_START, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_SUPPORTED_FEATURES, + CONF_TYPE, + STATE_OFF, + STATE_ON, ) -from homeassistant.const import ATTR_ENTITY_ID, CONF_TYPE, STATE_OFF, STATE_ON from homeassistant.core import split_entity_id import homeassistant.util.dt as dt_util @@ -193,14 +198,18 @@ async def test_valve_set_state(hass, hk_driver, events): assert events[-1].data[ATTR_VALUE] is None -async def test_vacuum_set_state(hass, hk_driver, events): +async def test_vacuum_set_state_with_returnhome_and_start_support( + hass, hk_driver, events +): """Test if Vacuum accessory and HA are updated accordingly.""" entity_id = "vacuum.roomba" - hass.states.async_set(entity_id, None) + hass.states.async_set( + entity_id, None, {ATTR_SUPPORTED_FEATURES: SUPPORT_RETURN_HOME | SUPPORT_START} + ) await hass.async_block_till_done() - acc = DockVacuum(hass, hk_driver, "DockVacuum", entity_id, 2, None) + acc = Vacuum(hass, hk_driver, "Vacuum", entity_id, 2, None) await acc.run_handler() await hass.async_block_till_done() assert acc.aid == 2 @@ -208,11 +217,19 @@ async def test_vacuum_set_state(hass, hk_driver, events): assert acc.char_on.value == 0 - hass.states.async_set(entity_id, STATE_CLEANING) + hass.states.async_set( + entity_id, + STATE_CLEANING, + {ATTR_SUPPORTED_FEATURES: SUPPORT_RETURN_HOME | SUPPORT_START}, + ) await hass.async_block_till_done() assert acc.char_on.value == 1 - hass.states.async_set(entity_id, STATE_DOCKED) + hass.states.async_set( + entity_id, + STATE_DOCKED, + {ATTR_SUPPORTED_FEATURES: SUPPORT_RETURN_HOME | SUPPORT_START}, + ) await hass.async_block_till_done() assert acc.char_on.value == 0 @@ -239,6 +256,52 @@ async def test_vacuum_set_state(hass, hk_driver, events): assert events[-1].data[ATTR_VALUE] is None +async def test_vacuum_set_state_without_returnhome_and_start_support( + hass, hk_driver, events +): + """Test if Vacuum accessory and HA are updated accordingly.""" + entity_id = "vacuum.roomba" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + + acc = Vacuum(hass, hk_driver, "Vacuum", entity_id, 2, None) + await acc.run_handler() + await hass.async_block_till_done() + assert acc.aid == 2 + assert acc.category == 8 # Switch + + assert acc.char_on.value == 0 + + hass.states.async_set(entity_id, STATE_ON) + await hass.async_block_till_done() + assert acc.char_on.value == 1 + + hass.states.async_set(entity_id, STATE_OFF) + await hass.async_block_till_done() + assert acc.char_on.value == 0 + + # Set from HomeKit + call_turn_on = async_mock_service(hass, VACUUM_DOMAIN, SERVICE_TURN_ON) + call_turn_off = async_mock_service(hass, VACUUM_DOMAIN, SERVICE_TURN_OFF) + + await hass.async_add_executor_job(acc.char_on.client_update_value, 1) + await hass.async_block_till_done() + assert acc.char_on.value == 1 + assert call_turn_on + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] is None + + await hass.async_add_executor_job(acc.char_on.client_update_value, 0) + await hass.async_block_till_done() + assert acc.char_on.value == 0 + assert call_turn_off + assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id + assert len(events) == 2 + assert events[-1].data[ATTR_VALUE] is None + + async def test_reset_switch(hass, hk_driver, events): """Test if switch accessory is reset correctly.""" domain = "scene" diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index acb45bca85f..ce17cf7ea07 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -1,4 +1,6 @@ """Test different accessory types: Thermostats.""" +from unittest.mock import patch + from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE import pytest @@ -61,7 +63,6 @@ from homeassistant.const import ( from homeassistant.core import CoreState from homeassistant.helpers import entity_registry -from tests.async_mock import patch from tests.common import async_mock_service diff --git a/tests/components/homekit/util.py b/tests/components/homekit/util.py index a9def6d02f7..8555be00aa7 100644 --- a/tests/components/homekit/util.py +++ b/tests/components/homekit/util.py @@ -1,10 +1,11 @@ """Test util for the homekit integration.""" +from unittest.mock import patch + from homeassistant.components.homekit.const import DOMAIN from homeassistant.const import CONF_NAME, CONF_PORT from homeassistant.core import HomeAssistant -from tests.async_mock import patch from tests.common import MockConfigEntry PATH_HOMEKIT = "homeassistant.components.homekit" diff --git a/tests/components/homekit_controller/conftest.py b/tests/components/homekit_controller/conftest.py index cb75fc205e2..26adb25df21 100644 --- a/tests/components/homekit_controller/conftest.py +++ b/tests/components/homekit_controller/conftest.py @@ -1,13 +1,13 @@ """HomeKit controller session fixtures.""" import datetime from unittest import mock +import unittest.mock from aiohomekit.testing import FakeController import pytest import homeassistant.util.dt as dt_util -import tests.async_mock from tests.components.light.conftest import mock_light_profiles # noqa @@ -25,5 +25,5 @@ def utcnow(request): def controller(hass): """Replace aiohomekit.Controller with an instance of aiohomekit.testing.FakeController.""" instance = FakeController() - with tests.async_mock.patch("aiohomekit.Controller", return_value=instance): + with unittest.mock.patch("aiohomekit.Controller", return_value=instance): yield instance diff --git a/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py b/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py new file mode 100644 index 00000000000..f97821ef111 --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_koogeek_p1eu.py @@ -0,0 +1,51 @@ +"""Make sure that existing Koogeek P1EU support isn't broken.""" + +from tests.components.homekit_controller.common import ( + Helper, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_koogeek_p1eu_setup(hass): + """Test that a Koogeek P1EU can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "koogeek_p1eu.json") + config_entry, pairing = await setup_test_accessories(hass, accessories) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + device_registry = await hass.helpers.device_registry.async_get_registry() + + # Check that the switch entity is handled correctly + + entry = entity_registry.async_get("switch.koogeek_p1_a00aa0") + assert entry.unique_id == "homekit-EUCP03190xxxxx48-7" + + helper = Helper( + hass, "switch.koogeek_p1_a00aa0", pairing, accessories[0], config_entry + ) + state = await helper.poll_and_get_state() + assert state.attributes["friendly_name"] == "Koogeek-P1-A00AA0" + + device = device_registry.async_get(entry.device_id) + assert device.manufacturer == "Koogeek" + assert device.name == "Koogeek-P1-A00AA0" + assert device.model == "P1EU" + assert device.sw_version == "2.3.7" + assert device.via_device_id is None + + # Assert the power sensor is detected + entry = entity_registry.async_get("sensor.koogeek_p1_a00aa0_real_time_energy") + assert entry.unique_id == "homekit-EUCP03190xxxxx48-aid:1-sid:21-cid:21" + + helper = Helper( + hass, + "sensor.koogeek_p1_a00aa0_real_time_energy", + pairing, + accessories[0], + config_entry, + ) + state = await helper.poll_and_get_state() + assert state.attributes["friendly_name"] == "Koogeek-P1-A00AA0 - Real Time Energy" + + # The sensor and switch should be part of the same device + assert entry.device_id == device.id diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 72a8133159d..9cc785f85fb 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -1,5 +1,7 @@ """Tests for homekit_controller config flow.""" from unittest import mock +import unittest.mock +from unittest.mock import patch import aiohomekit from aiohomekit.model import Accessories, Accessory @@ -10,8 +12,6 @@ import pytest from homeassistant.components.homekit_controller import config_flow from homeassistant.helpers import device_registry -import tests.async_mock -from tests.async_mock import patch from tests.common import MockConfigEntry, mock_device_registry PAIRING_START_FORM_ERRORS = [ @@ -71,15 +71,15 @@ def _setup_flow_handler(hass, pairing=None): flow.hass = hass flow.context = {} - finish_pairing = tests.async_mock.AsyncMock(return_value=pairing) + finish_pairing = unittest.mock.AsyncMock(return_value=pairing) discovery = mock.Mock() discovery.device_id = "00:00:00:00:00:00" - discovery.start_pairing = tests.async_mock.AsyncMock(return_value=finish_pairing) + discovery.start_pairing = unittest.mock.AsyncMock(return_value=finish_pairing) flow.controller = mock.Mock() flow.controller.pairings = {} - flow.controller.find_ip_by_device_id = tests.async_mock.AsyncMock( + flow.controller.find_ip_by_device_id = unittest.mock.AsyncMock( return_value=discovery ) @@ -475,7 +475,7 @@ async def test_pair_abort_errors_on_finish(hass, controller, exception, expected # User initiates pairing - this triggers the device to show a pairing code # and then HA to show a pairing form - finish_pairing = tests.async_mock.AsyncMock(side_effect=exception("error")) + finish_pairing = unittest.mock.AsyncMock(side_effect=exception("error")) with patch.object(device, "start_pairing", return_value=finish_pairing): result = await hass.config_entries.flow.async_configure(result["flow_id"]) @@ -515,7 +515,7 @@ async def test_pair_form_errors_on_finish(hass, controller, exception, expected) # User initiates pairing - this triggers the device to show a pairing code # and then HA to show a pairing form - finish_pairing = tests.async_mock.AsyncMock(side_effect=exception("error")) + finish_pairing = unittest.mock.AsyncMock(side_effect=exception("error")) with patch.object(device, "start_pairing", return_value=finish_pairing): result = await hass.config_entries.flow.async_configure(result["flow_id"]) diff --git a/tests/components/homekit_controller/test_sensor.py b/tests/components/homekit_controller/test_sensor.py index 8b0528ea46d..a79e94c4bb7 100644 --- a/tests/components/homekit_controller/test_sensor.py +++ b/tests/components/homekit_controller/test_sensor.py @@ -9,7 +9,7 @@ from homeassistant.const import ( DEVICE_CLASS_TEMPERATURE, ) -from tests.components.homekit_controller.common import setup_test_component +from tests.components.homekit_controller.common import Helper, setup_test_component TEMPERATURE = ("temperature", "temperature.current") HUMIDITY = ("humidity", "relative-humidity.current") @@ -18,6 +18,7 @@ CARBON_DIOXIDE_LEVEL = ("carbon-dioxide", "carbon-dioxide.level") BATTERY_LEVEL = ("battery", "battery-level") CHARGING_STATE = ("battery", "charging-state") LO_BATT = ("battery", "status-lo-batt") +ON = ("outlet", "on") def create_temperature_sensor_service(accessory): @@ -183,3 +184,45 @@ async def test_battery_low(hass, utcnow): helper.characteristics[LO_BATT].value = 1 state = await helper.poll_and_get_state() assert state.attributes["icon"] == "mdi:battery-alert" + + +def create_switch_with_sensor(accessory): + """Define battery level characteristics.""" + service = accessory.add_service(ServicesTypes.OUTLET) + + realtime_energy = service.add_char( + CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY + ) + realtime_energy.value = 0 + realtime_energy.format = "float" + + cur_state = service.add_char(CharacteristicsTypes.ON) + cur_state.value = True + + return service + + +async def test_switch_with_sensor(hass, utcnow): + """Test a switch service that has a sensor characteristic is correctly handled.""" + helper = await setup_test_component(hass, create_switch_with_sensor) + outlet = helper.accessory.services.first(service_type=ServicesTypes.OUTLET) + + # Helper will be for the primary entity, which is the outlet. Make a helper for the sensor. + energy_helper = Helper( + hass, + "sensor.testdevice_real_time_energy", + helper.pairing, + helper.accessory, + helper.config_entry, + ) + + outlet = energy_helper.accessory.services.first(service_type=ServicesTypes.OUTLET) + realtime_energy = outlet[CharacteristicsTypes.Vendor.KOOGEEK_REALTIME_ENERGY] + + realtime_energy.value = 1 + state = await energy_helper.poll_and_get_state() + assert state.state == "1" + + realtime_energy.value = 50 + state = await energy_helper.poll_and_get_state() + assert state.state == "50" diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index 9764ee74e22..b05683d2361 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -1,4 +1,6 @@ """Initializer helpers for HomematicIP fake server.""" +from unittest.mock import AsyncMock, MagicMock, Mock, patch + from homematicip.aio.auth import AsyncAuth from homematicip.aio.connection import AsyncConnection from homematicip.aio.home import AsyncHome @@ -22,7 +24,6 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType from .helper import AUTH_TOKEN, HAPID, HAPPIN, HomeFactory -from tests.async_mock import AsyncMock, MagicMock, Mock, patch from tests.common import MockConfigEntry from tests.components.light.conftest import mock_light_profiles # noqa @@ -46,7 +47,7 @@ def mock_connection_fixture() -> AsyncConnection: @pytest.fixture(name="hmip_config_entry") def hmip_config_entry_fixture() -> config_entries.ConfigEntry: - """Create a mock config entriy for homematic ip cloud.""" + """Create a mock config entry for homematic ip cloud.""" entry_data = { HMIPC_HAPID: HAPID, HMIPC_AUTHTOKEN: AUTH_TOKEN, diff --git a/tests/components/homematicip_cloud/helper.py b/tests/components/homematicip_cloud/helper.py index ca7d8862756..8da5e4861c0 100644 --- a/tests/components/homematicip_cloud/helper.py +++ b/tests/components/homematicip_cloud/helper.py @@ -1,5 +1,6 @@ """Helper for HomematicIP Cloud Tests.""" import json +from unittest.mock import Mock, patch from homematicip.aio.class_maps import ( TYPE_CLASS_MAP, @@ -21,7 +22,6 @@ from homeassistant.components.homematicip_cloud.hap import HomematicipHAP from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch from tests.common import load_fixture HAPID = "3014F7110000000000000001" diff --git a/tests/components/homematicip_cloud/test_climate.py b/tests/components/homematicip_cloud/test_climate.py index 6d5c3fb6060..dc850fac026 100644 --- a/tests/components/homematicip_cloud/test_climate.py +++ b/tests/components/homematicip_cloud/test_climate.py @@ -129,7 +129,7 @@ async def test_hmip_heating_group_heat(hass, default_mock_hap_factory): ha_state = hass.states.get(entity_id) assert ha_state.attributes[ATTR_PRESET_MODE] == "STD" - # Not required for hmip, but a posiblity to send no temperature. + # Not required for hmip, but a possibility to send no temperature. await hass.services.async_call( "climate", "set_temperature", diff --git a/tests/components/homematicip_cloud/test_config_flow.py b/tests/components/homematicip_cloud/test_config_flow.py index e9ecab2dbfb..0b573e66b1d 100644 --- a/tests/components/homematicip_cloud/test_config_flow.py +++ b/tests/components/homematicip_cloud/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for HomematicIP Cloud config flow.""" +from unittest.mock import patch + from homeassistant.components.homematicip_cloud.const import ( DOMAIN as HMIPC_DOMAIN, HMIPC_AUTHTOKEN, @@ -7,7 +9,6 @@ from homeassistant.components.homematicip_cloud.const import ( HMIPC_PIN, ) -from tests.async_mock import patch from tests.common import MockConfigEntry DEFAULT_CONFIG = {HMIPC_HAPID: "ABC123", HMIPC_PIN: "123", HMIPC_NAME: "hmip"} diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 0e69a67cdbf..264d93c4145 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -1,4 +1,6 @@ """Common tests for HomematicIP devices.""" +from unittest.mock import patch + from homematicip.base.enums import EventType from homeassistant.components.homematicip_cloud import DOMAIN as HMIPC_DOMAIN @@ -13,8 +15,6 @@ from .helper import ( get_and_check_entity_basics, ) -from tests.async_mock import patch - async def test_hmip_load_all_supported_devices(hass, default_mock_hap_factory): """Ensure that all supported devices could be loaded.""" @@ -22,7 +22,7 @@ async def test_hmip_load_all_supported_devices(hass, default_mock_hap_factory): test_devices=None, test_groups=None ) - assert len(mock_hap.hmip_device_by_entity_id) == 250 + assert len(mock_hap.hmip_device_by_entity_id) == 253 async def test_hmip_remove_device(hass, default_mock_hap_factory): diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index 2a4833553d2..6c95017a635 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -1,5 +1,7 @@ """Test HomematicIP Cloud accesspoint.""" +from unittest.mock import Mock, patch + from homematicip.aio.auth import AsyncAuth from homematicip.base.base_connection import HmipConnectionError import pytest @@ -21,8 +23,6 @@ from homeassistant.exceptions import ConfigEntryNotReady from .helper import HAPID, HAPPIN -from tests.async_mock import Mock, patch - async def test_auth_setup(hass): """Test auth setup for client registration.""" diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py index 46059f12d00..250cba81637 100644 --- a/tests/components/homematicip_cloud/test_init.py +++ b/tests/components/homematicip_cloud/test_init.py @@ -1,5 +1,7 @@ """Test HomematicIP Cloud setup process.""" +from unittest.mock import AsyncMock, Mock, patch + from homematicip.base.base_connection import HmipConnectionError from homeassistant.components.homematicip_cloud.const import ( @@ -20,7 +22,6 @@ from homeassistant.config_entries import ( from homeassistant.const import CONF_NAME from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/html5/test_notify.py b/tests/components/html5/test_notify.py index d3faa761435..4a62eb76c27 100644 --- a/tests/components/html5/test_notify.py +++ b/tests/components/html5/test_notify.py @@ -1,5 +1,6 @@ """Test HTML5 notify platform.""" import json +from unittest.mock import MagicMock, mock_open, patch from aiohttp.hdrs import AUTHORIZATION @@ -8,8 +9,6 @@ from homeassistant.const import HTTP_INTERNAL_SERVER_ERROR from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, mock_open, patch - CONFIG_FILE = "file.conf" VAPID_CONF = { diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index e3274ddfa7d..71c01630a67 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -1,6 +1,7 @@ """The tests for the Home Assistant HTTP component.""" from datetime import timedelta from ipaddress import ip_network +from unittest.mock import patch from aiohttp import BasicAuth, web from aiohttp.web_exceptions import HTTPUnauthorized @@ -14,8 +15,6 @@ from homeassistant.setup import async_setup_component from . import HTTP_HEADER_HA_AUTH, mock_real_ip -from tests.async_mock import patch - API_PASSWORD = "test-password" # Don't add 127.0.0.1/::1 as trusted, as it may interfere with other test cases diff --git a/tests/components/http/test_ban.py b/tests/components/http/test_ban.py index 993ec708c18..717bd9564c0 100644 --- a/tests/components/http/test_ban.py +++ b/tests/components/http/test_ban.py @@ -2,6 +2,7 @@ # pylint: disable=protected-access from ipaddress import ip_address import os +from unittest.mock import Mock, mock_open, patch from aiohttp import web from aiohttp.web_exceptions import HTTPUnauthorized @@ -23,7 +24,6 @@ from homeassistant.setup import async_setup_component from . import mock_real_ip -from tests.async_mock import Mock, mock_open, patch from tests.common import async_mock_service SUPERVISOR_IP = "1.2.3.4" @@ -174,8 +174,8 @@ async def test_ip_bans_file_creation(hass, aiohttp_client): assert len(notification_calls) == 3 assert ( - "Login attempt or request with invalid authentication from example.com (200.201.202.204) (Python" - in notification_calls[0].data["message"] + notification_calls[0].data["message"] + == "Login attempt or request with invalid authentication from example.com (200.201.202.204). See the log for details." ) diff --git a/tests/components/http/test_cors.py b/tests/components/http/test_cors.py index 191cdb0ba49..04447191fd5 100644 --- a/tests/components/http/test_cors.py +++ b/tests/components/http/test_cors.py @@ -1,5 +1,6 @@ """Test cors for the HTTP component.""" from pathlib import Path +from unittest.mock import patch from aiohttp import web from aiohttp.hdrs import ( @@ -18,8 +19,6 @@ from homeassistant.setup import async_setup_component from . import HTTP_HEADER_HA_AUTH -from tests.async_mock import patch - TRUSTED_ORIGIN = "https://home-assistant.io" diff --git a/tests/components/http/test_data_validator.py b/tests/components/http/test_data_validator.py index c7b5ed42ccd..a6e812ccdfe 100644 --- a/tests/components/http/test_data_validator.py +++ b/tests/components/http/test_data_validator.py @@ -1,12 +1,12 @@ """Test data validator decorator.""" +from unittest.mock import Mock + from aiohttp import web import voluptuous as vol from homeassistant.components.http import HomeAssistantView from homeassistant.components.http.data_validator import RequestDataValidator -from tests.async_mock import Mock - async def get_client(aiohttp_client, validator): """Generate a client that hits a view decorated with validator.""" diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 2c95d03a9ef..3dd587cd7a4 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -1,6 +1,7 @@ """The tests for the Home Assistant HTTP component.""" from ipaddress import ip_network import logging +from unittest.mock import Mock, patch import pytest @@ -8,8 +9,6 @@ import homeassistant.components.http as http from homeassistant.setup import async_setup_component from homeassistant.util.ssl import server_context_intermediate, server_context_modern -from tests.async_mock import Mock, patch - @pytest.fixture def mock_stack(): diff --git a/tests/components/http/test_view.py b/tests/components/http/test_view.py index 045f0837983..522344461cb 100644 --- a/tests/components/http/test_view.py +++ b/tests/components/http/test_view.py @@ -1,4 +1,6 @@ """Tests for Home Assistant View.""" +from unittest.mock import AsyncMock, Mock + from aiohttp.web_exceptions import ( HTTPBadRequest, HTTPInternalServerError, @@ -13,8 +15,6 @@ from homeassistant.components.http.view import ( ) from homeassistant.exceptions import ServiceNotFound, Unauthorized -from tests.async_mock import AsyncMock, Mock - @pytest.fixture def mock_request(): diff --git a/tests/components/huawei_lte/test_config_flow.py b/tests/components/huawei_lte/test_config_flow.py index 2547aa3f01f..baffcef3476 100644 --- a/tests/components/huawei_lte/test_config_flow.py +++ b/tests/components/huawei_lte/test_config_flow.py @@ -1,5 +1,7 @@ """Tests for the Huawei LTE config flow.""" +from unittest.mock import patch + from huawei_lte_api.enums.client import ResponseCodeEnum from huawei_lte_api.enums.user import LoginErrorEnum, LoginStateEnum, PasswordTypeEnum import pytest @@ -17,7 +19,6 @@ from homeassistant.const import ( CONF_USERNAME, ) -from tests.async_mock import patch from tests.common import MockConfigEntry FIXTURE_USER_INPUT = { diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py index 55b6443bdfa..fc42babbb35 100644 --- a/tests/components/hue/conftest.py +++ b/tests/components/hue/conftest.py @@ -1,5 +1,6 @@ """Test helpers for Hue.""" from collections import deque +from unittest.mock import AsyncMock, Mock, patch from aiohue.groups import Groups from aiohue.lights import Lights @@ -11,7 +12,6 @@ from homeassistant import config_entries from homeassistant.components import hue from homeassistant.components.hue import sensor_base as hue_sensor_base -from tests.async_mock import AsyncMock, Mock, patch from tests.components.light.conftest import mock_light_profiles # noqa diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index 06dd459723d..3e6465d6bc8 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -1,4 +1,6 @@ """Test Hue bridge.""" +from unittest.mock import AsyncMock, Mock, patch + import pytest from homeassistant import config_entries @@ -10,8 +12,6 @@ from homeassistant.components.hue.const import ( ) from homeassistant.exceptions import ConfigEntryNotReady -from tests.async_mock import AsyncMock, Mock, patch - async def test_bridge_setup(hass): """Test a successful setup.""" diff --git a/tests/components/hue/test_config_flow.py b/tests/components/hue/test_config_flow.py index a551e623e63..c7dc83183ae 100644 --- a/tests/components/hue/test_config_flow.py +++ b/tests/components/hue/test_config_flow.py @@ -1,5 +1,6 @@ """Tests for Philips Hue config flow.""" import asyncio +from unittest.mock import AsyncMock, Mock, patch from aiohttp import client_exceptions import aiohue @@ -11,7 +12,6 @@ from homeassistant import config_entries from homeassistant.components import ssdp from homeassistant.components.hue import config_flow, const -from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/hue/test_device_trigger.py b/tests/components/hue/test_device_trigger.py index 0975c644e61..178cd7b09f1 100644 --- a/tests/components/hue/test_device_trigger.py +++ b/tests/components/hue/test_device_trigger.py @@ -43,7 +43,7 @@ async def test_get_triggers(hass, mock_bridge, device_reg): # Get triggers for specific tap switch hue_tap_device = device_reg.async_get_device( - {(hue.DOMAIN, "00:00:00:00:00:44:23:08")}, connections={} + {(hue.DOMAIN, "00:00:00:00:00:44:23:08")} ) triggers = await async_get_device_automations(hass, "trigger", hue_tap_device.id) @@ -61,7 +61,7 @@ async def test_get_triggers(hass, mock_bridge, device_reg): # Get triggers for specific dimmer switch hue_dimmer_device = device_reg.async_get_device( - {(hue.DOMAIN, "00:17:88:01:10:3e:3a:dc")}, connections={} + {(hue.DOMAIN, "00:17:88:01:10:3e:3a:dc")} ) triggers = await async_get_device_automations(hass, "trigger", hue_dimmer_device.id) @@ -97,7 +97,7 @@ async def test_if_fires_on_state_change(hass, mock_bridge, device_reg, calls): # Set an automation with a specific tap switch trigger hue_tap_device = device_reg.async_get_device( - {(hue.DOMAIN, "00:00:00:00:00:44:23:08")}, connections={} + {(hue.DOMAIN, "00:00:00:00:00:44:23:08")} ) assert await async_setup_component( hass, diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index 70a6c3b8756..1f6ba83e2ca 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -1,5 +1,5 @@ """Test Hue setup process.""" -from unittest.mock import Mock +from unittest.mock import AsyncMock, Mock, patch import pytest @@ -7,7 +7,6 @@ from homeassistant import config_entries from homeassistant.components import hue from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry diff --git a/tests/components/hue/test_init_multiple_bridges.py b/tests/components/hue/test_init_multiple_bridges.py index 32be7677398..19b4da44a4d 100644 --- a/tests/components/hue/test_init_multiple_bridges.py +++ b/tests/components/hue/test_init_multiple_bridges.py @@ -1,5 +1,7 @@ """Test Hue init with multiple bridges.""" +from unittest.mock import Mock, patch + from aiohue.groups import Groups from aiohue.lights import Lights from aiohue.scenes import Scenes @@ -11,8 +13,6 @@ from homeassistant.components import hue from homeassistant.components.hue import sensor_base as hue_sensor_base from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch - async def setup_component(hass): """Hue component.""" diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py index 2029c32c82f..629a9a4c98b 100644 --- a/tests/components/hue/test_light.py +++ b/tests/components/hue/test_light.py @@ -1,5 +1,6 @@ """Philips Hue lights platform tests.""" import asyncio +from unittest.mock import Mock import aiohue @@ -8,8 +9,6 @@ from homeassistant.components import hue from homeassistant.components.hue import light as hue_light from homeassistant.util import color -from tests.async_mock import Mock - HUE_LIGHT_NS = "homeassistant.components.light.hue." GROUP_RESPONSE = { "1": { diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py index fcf3513bc7c..eb7ece241c3 100644 --- a/tests/components/hue/test_sensor_base.py +++ b/tests/components/hue/test_sensor_base.py @@ -1,5 +1,6 @@ """Philips Hue sensors platform tests.""" import asyncio +from unittest.mock import Mock import aiohue @@ -7,8 +8,6 @@ from homeassistant.components.hue.hue_event import CONF_HUE_EVENT from .conftest import create_mock_bridge, setup_bridge_for_sensors as setup_bridge -from tests.async_mock import Mock - PRESENCE_SENSOR_1_PRESENT = { "state": {"presence": True, "lastupdated": "2019-01-01T01:00:00"}, "swupdate": {"state": "noupdates", "lastinstall": "2019-01-01T00:00:00"}, diff --git a/tests/components/huisbaasje/__init__.py b/tests/components/huisbaasje/__init__.py new file mode 100644 index 00000000000..8cf2749d8df --- /dev/null +++ b/tests/components/huisbaasje/__init__.py @@ -0,0 +1 @@ +"""Tests for the Huisbaasje integration.""" diff --git a/tests/components/huisbaasje/test_config_flow.py b/tests/components/huisbaasje/test_config_flow.py new file mode 100644 index 00000000000..245ac2f8ddb --- /dev/null +++ b/tests/components/huisbaasje/test_config_flow.py @@ -0,0 +1,157 @@ +"""Test the Huisbaasje config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.huisbaasje.config_flow import ( + HuisbaasjeConnectionException, + HuisbaasjeException, +) +from homeassistant.components.huisbaasje.const import DOMAIN + +from tests.common import MockConfigEntry + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "huisbaasje.Huisbaasje.authenticate", return_value=None + ) as mock_authenticate, patch( + "huisbaasje.Huisbaasje.get_user_id", + return_value="test-id", + ) as mock_get_user_id, patch( + "homeassistant.components.huisbaasje.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.huisbaasje.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + form_result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + await hass.async_block_till_done() + + assert form_result["type"] == "create_entry" + assert form_result["title"] == "test-username" + assert form_result["data"] == { + "id": "test-id", + "username": "test-username", + "password": "test-password", + } + assert len(mock_authenticate.mock_calls) == 1 + assert len(mock_get_user_id.mock_calls) == 1 + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "huisbaasje.Huisbaasje.authenticate", + side_effect=HuisbaasjeException, + ): + form_result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + + assert form_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert form_result["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "huisbaasje.Huisbaasje.authenticate", + side_effect=HuisbaasjeConnectionException, + ): + form_result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + + assert form_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert form_result["errors"] == {"base": "connection_exception"} + + +async def test_form_unknown_error(hass): + """Test we handle an unknown error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "huisbaasje.Huisbaasje.authenticate", + side_effect=Exception, + ): + form_result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + + assert form_result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert form_result["errors"] == {"base": "unknown"} + + +async def test_form_entry_exists(hass): + """Test we handle an already existing entry.""" + MockConfigEntry( + unique_id="test-id", + domain=DOMAIN, + data={ + "id": "test-id", + "username": "test-username", + "password": "test-password", + }, + title="test-username", + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("huisbaasje.Huisbaasje.authenticate", return_value=None), patch( + "huisbaasje.Huisbaasje.get_user_id", + return_value="test-id", + ), patch( + "homeassistant.components.huisbaasje.async_setup", return_value=True + ), patch( + "homeassistant.components.huisbaasje.async_setup_entry", + return_value=True, + ): + form_result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "username": "test-username", + "password": "test-password", + }, + ) + + assert form_result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert form_result["reason"] == "already_configured" diff --git a/tests/components/huisbaasje/test_data.py b/tests/components/huisbaasje/test_data.py new file mode 100644 index 00000000000..5752be11c51 --- /dev/null +++ b/tests/components/huisbaasje/test_data.py @@ -0,0 +1,79 @@ +"""Test data for the tests of the Huisbaasje integration.""" +MOCK_CURRENT_MEASUREMENTS = { + "electricity": { + "measurement": { + "time": "2020-11-18T15:17:24.000Z", + "rate": 1011.6666666666667, + "value": 0.0033333333333333335, + "costPerHour": 0.20233333333333337, + "counterValue": 409.17166666631937, + }, + "thisDay": {"value": 3.296665869, "cost": 0.6593331738}, + "thisWeek": {"value": 17.509996085, "cost": 3.5019992170000003}, + "thisMonth": {"value": 103.28830788, "cost": 20.657661576000002}, + "thisYear": {"value": 672.9781177300001, "cost": 134.595623546}, + }, + "electricityIn": { + "measurement": { + "time": "2020-11-18T15:17:24.000Z", + "rate": 1011.6666666666667, + "value": 0.0033333333333333335, + "costPerHour": 0.20233333333333337, + "counterValue": 409.17166666631937, + }, + "thisDay": {"value": 2.669999453, "cost": 0.5339998906}, + "thisWeek": {"value": 15.328330291, "cost": 3.0656660582}, + "thisMonth": {"value": 72.986651896, "cost": 14.5973303792}, + "thisYear": {"value": 409.214880212, "cost": 81.84297604240001}, + }, + "electricityInLow": { + "measurement": None, + "thisDay": {"value": 0.6266664160000001, "cost": 0.1253332832}, + "thisWeek": {"value": 2.181665794, "cost": 0.43633315880000006}, + "thisMonth": {"value": 30.301655984000003, "cost": 6.060331196800001}, + "thisYear": {"value": 263.76323751800004, "cost": 52.75264750360001}, + }, + "electricityOut": { + "measurement": None, + "thisDay": {"value": 0.0, "cost": 0.0}, + "thisWeek": {"value": 0.0, "cost": 0.0}, + "thisMonth": {"value": 0.0, "cost": 0.0}, + "thisYear": {"value": 0.0, "cost": 0.0}, + }, + "electricityOutLow": { + "measurement": None, + "thisDay": {"value": 0.0, "cost": 0.0}, + "thisWeek": {"value": 0.0, "cost": 0.0}, + "thisMonth": {"value": 0.0, "cost": 0.0}, + "thisYear": {"value": 0.0, "cost": 0.0}, + }, + "gas": { + "measurement": { + "time": "2020-11-18T15:17:29.000Z", + "rate": 0.0, + "value": 0.0, + "costPerHour": 0.0, + "counterValue": 116.73000000002281, + }, + "thisDay": {"value": 1.07, "cost": 0.642}, + "thisWeek": {"value": 5.634224386000001, "cost": 3.3805346316000007}, + "thisMonth": {"value": 39.14, "cost": 23.483999999999998}, + "thisYear": {"value": 116.73, "cost": 70.038}, + }, +} + +MOCK_LIMITED_CURRENT_MEASUREMENTS = { + "electricity": { + "measurement": { + "time": "2020-11-18T15:17:24.000Z", + "rate": 1011.6666666666667, + "value": 0.0033333333333333335, + "costPerHour": 0.20233333333333337, + "counterValue": 409.17166666631937, + }, + "thisDay": {"value": 3.296665869, "cost": 0.6593331738}, + "thisWeek": {"value": 17.509996085, "cost": 3.5019992170000003}, + "thisMonth": {"value": 103.28830788, "cost": 20.657661576000002}, + "thisYear": {"value": 672.9781177300001, "cost": 134.595623546}, + } +} diff --git a/tests/components/huisbaasje/test_init.py b/tests/components/huisbaasje/test_init.py new file mode 100644 index 00000000000..96be450f7e4 --- /dev/null +++ b/tests/components/huisbaasje/test_init.py @@ -0,0 +1,153 @@ +"""Test cases for the initialisation of the Huisbaasje integration.""" +from unittest.mock import patch + +from huisbaasje import HuisbaasjeException + +from homeassistant.components import huisbaasje +from homeassistant.config_entries import ( + CONN_CLASS_CLOUD_POLL, + ENTRY_STATE_LOADED, + ENTRY_STATE_NOT_LOADED, + ENTRY_STATE_SETUP_ERROR, + ConfigEntry, +) +from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.components.huisbaasje.test_data import MOCK_CURRENT_MEASUREMENTS + + +async def test_setup(hass: HomeAssistant): + """Test for successfully setting up the platform.""" + assert await async_setup_component(hass, huisbaasje.DOMAIN, {}) + await hass.async_block_till_done() + assert huisbaasje.DOMAIN in hass.config.components + + +async def test_setup_entry(hass: HomeAssistant): + """Test for successfully setting a config entry.""" + with patch( + "huisbaasje.Huisbaasje.authenticate", return_value=None + ) as mock_authenticate, patch( + "huisbaasje.Huisbaasje.is_authenticated", return_value=True + ) as mock_is_authenticated, patch( + "huisbaasje.Huisbaasje.current_measurements", + return_value=MOCK_CURRENT_MEASUREMENTS, + ) as mock_current_measurements: + hass.config.components.add(huisbaasje.DOMAIN) + config_entry = ConfigEntry( + 1, + huisbaasje.DOMAIN, + "userId", + { + CONF_ID: "userId", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + }, + "test", + CONN_CLASS_CLOUD_POLL, + system_options={}, + ) + hass.config_entries._entries.append(config_entry) + + assert config_entry.state == ENTRY_STATE_NOT_LOADED + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Assert integration is loaded + assert config_entry.state == ENTRY_STATE_LOADED + assert huisbaasje.DOMAIN in hass.config.components + assert huisbaasje.DOMAIN in hass.data + assert config_entry.entry_id in hass.data[huisbaasje.DOMAIN] + + # Assert entities are loaded + entities = hass.states.async_entity_ids("sensor") + assert len(entities) == 14 + + # Assert mocks are called + assert len(mock_authenticate.mock_calls) == 1 + assert len(mock_is_authenticated.mock_calls) == 1 + assert len(mock_current_measurements.mock_calls) == 1 + + +async def test_setup_entry_error(hass: HomeAssistant): + """Test for successfully setting a config entry.""" + with patch( + "huisbaasje.Huisbaasje.authenticate", side_effect=HuisbaasjeException + ) as mock_authenticate: + hass.config.components.add(huisbaasje.DOMAIN) + config_entry = ConfigEntry( + 1, + huisbaasje.DOMAIN, + "userId", + { + CONF_ID: "userId", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + }, + "test", + CONN_CLASS_CLOUD_POLL, + system_options={}, + ) + hass.config_entries._entries.append(config_entry) + + assert config_entry.state == ENTRY_STATE_NOT_LOADED + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Assert integration is loaded with error + assert config_entry.state == ENTRY_STATE_SETUP_ERROR + assert huisbaasje.DOMAIN not in hass.data + + # Assert entities are not loaded + entities = hass.states.async_entity_ids("sensor") + assert len(entities) == 0 + + # Assert mocks are called + assert len(mock_authenticate.mock_calls) == 1 + + +async def test_unload_entry(hass: HomeAssistant): + """Test for successfully unloading the config entry.""" + with patch( + "huisbaasje.Huisbaasje.authenticate", return_value=None + ) as mock_authenticate, patch( + "huisbaasje.Huisbaasje.is_authenticated", return_value=True + ) as mock_is_authenticated, patch( + "huisbaasje.Huisbaasje.current_measurements", + return_value=MOCK_CURRENT_MEASUREMENTS, + ) as mock_current_measurements: + hass.config.components.add(huisbaasje.DOMAIN) + config_entry = ConfigEntry( + 1, + huisbaasje.DOMAIN, + "userId", + { + CONF_ID: "userId", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + }, + "test", + CONN_CLASS_CLOUD_POLL, + system_options={}, + ) + hass.config_entries._entries.append(config_entry) + + # Load config entry + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert config_entry.state == ENTRY_STATE_LOADED + entities = hass.states.async_entity_ids("sensor") + assert len(entities) == 14 + + # Unload config entry + await hass.config_entries.async_unload(config_entry.entry_id) + assert config_entry.state == ENTRY_STATE_NOT_LOADED + entities = hass.states.async_entity_ids("sensor") + assert len(entities) == 0 + + # Assert mocks are called + assert len(mock_authenticate.mock_calls) == 1 + assert len(mock_is_authenticated.mock_calls) == 1 + assert len(mock_current_measurements.mock_calls) == 1 diff --git a/tests/components/huisbaasje/test_sensor.py b/tests/components/huisbaasje/test_sensor.py new file mode 100644 index 00000000000..d1ffe565c84 --- /dev/null +++ b/tests/components/huisbaasje/test_sensor.py @@ -0,0 +1,120 @@ +"""Test cases for the sensors of the Huisbaasje integration.""" +from unittest.mock import patch + +from homeassistant.components import huisbaasje +from homeassistant.config_entries import CONN_CLASS_CLOUD_POLL, ConfigEntry +from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant + +from tests.components.huisbaasje.test_data import ( + MOCK_CURRENT_MEASUREMENTS, + MOCK_LIMITED_CURRENT_MEASUREMENTS, +) + + +async def test_setup_entry(hass: HomeAssistant): + """Test for successfully loading sensor states.""" + with patch( + "huisbaasje.Huisbaasje.authenticate", return_value=None + ) as mock_authenticate, patch( + "huisbaasje.Huisbaasje.is_authenticated", return_value=True + ) as mock_is_authenticated, patch( + "huisbaasje.Huisbaasje.current_measurements", + return_value=MOCK_CURRENT_MEASUREMENTS, + ) as mock_current_measurements: + + hass.config.components.add(huisbaasje.DOMAIN) + config_entry = ConfigEntry( + 1, + huisbaasje.DOMAIN, + "userId", + { + CONF_ID: "userId", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + }, + "test", + CONN_CLASS_CLOUD_POLL, + system_options={}, + ) + hass.config_entries._entries.append(config_entry) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Assert data is loaded + assert hass.states.get("sensor.huisbaasje_current_power").state == "1012.0" + assert hass.states.get("sensor.huisbaasje_current_power_in").state == "1012.0" + assert ( + hass.states.get("sensor.huisbaasje_current_power_in_low").state == "unknown" + ) + assert hass.states.get("sensor.huisbaasje_current_power_out").state == "unknown" + assert ( + hass.states.get("sensor.huisbaasje_current_power_out_low").state + == "unknown" + ) + assert hass.states.get("sensor.huisbaasje_current_gas").state == "0.0" + assert hass.states.get("sensor.huisbaasje_energy_today").state == "3.3" + assert hass.states.get("sensor.huisbaasje_energy_this_week").state == "17.5" + assert hass.states.get("sensor.huisbaasje_energy_this_month").state == "103.3" + assert hass.states.get("sensor.huisbaasje_energy_this_year").state == "673.0" + assert hass.states.get("sensor.huisbaasje_gas_today").state == "1.1" + assert hass.states.get("sensor.huisbaasje_gas_this_week").state == "5.6" + assert hass.states.get("sensor.huisbaasje_gas_this_month").state == "39.1" + assert hass.states.get("sensor.huisbaasje_gas_this_year").state == "116.7" + + # Assert mocks are called + assert len(mock_authenticate.mock_calls) == 1 + assert len(mock_is_authenticated.mock_calls) == 1 + assert len(mock_current_measurements.mock_calls) == 1 + + +async def test_setup_entry_absent_measurement(hass: HomeAssistant): + """Test for successfully loading sensor states when response does not contain all measurements.""" + with patch( + "huisbaasje.Huisbaasje.authenticate", return_value=None + ) as mock_authenticate, patch( + "huisbaasje.Huisbaasje.is_authenticated", return_value=True + ) as mock_is_authenticated, patch( + "huisbaasje.Huisbaasje.current_measurements", + return_value=MOCK_LIMITED_CURRENT_MEASUREMENTS, + ) as mock_current_measurements: + + hass.config.components.add(huisbaasje.DOMAIN) + config_entry = ConfigEntry( + 1, + huisbaasje.DOMAIN, + "userId", + { + CONF_ID: "userId", + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + }, + "test", + CONN_CLASS_CLOUD_POLL, + system_options={}, + ) + hass.config_entries._entries.append(config_entry) + + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Assert data is loaded + assert hass.states.get("sensor.huisbaasje_current_power").state == "1012.0" + assert hass.states.get("sensor.huisbaasje_current_power_in").state == "unknown" + assert ( + hass.states.get("sensor.huisbaasje_current_power_in_low").state == "unknown" + ) + assert hass.states.get("sensor.huisbaasje_current_power_out").state == "unknown" + assert ( + hass.states.get("sensor.huisbaasje_current_power_out_low").state + == "unknown" + ) + assert hass.states.get("sensor.huisbaasje_current_gas").state == "unknown" + assert hass.states.get("sensor.huisbaasje_energy_today").state == "3.3" + assert hass.states.get("sensor.huisbaasje_gas_today").state == "unknown" + + # Assert mocks are called + assert len(mock_authenticate.mock_calls) == 1 + assert len(mock_is_authenticated.mock_calls) == 1 + assert len(mock_current_measurements.mock_calls) == 1 diff --git a/tests/components/hunterdouglas_powerview/test_config_flow.py b/tests/components/hunterdouglas_powerview/test_config_flow.py index 84457a14a5e..b5b9ee84f27 100644 --- a/tests/components/hunterdouglas_powerview/test_config_flow.py +++ b/tests/components/hunterdouglas_powerview/test_config_flow.py @@ -1,11 +1,11 @@ """Test the Logitech Harmony Hub config flow.""" import asyncio import json +from unittest.mock import AsyncMock, MagicMock, patch from homeassistant import config_entries, setup from homeassistant.components.hunterdouglas_powerview.const import DOMAIN -from tests.async_mock import AsyncMock, MagicMock, patch from tests.common import MockConfigEntry, load_fixture @@ -69,37 +69,6 @@ async def test_user_form(hass): assert result4["type"] == "abort" -async def test_form_import(hass): - """Test we get the form with import source.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - mock_powerview_userdata = _get_mock_powerview_userdata() - with patch( - "homeassistant.components.hunterdouglas_powerview.UserData", - return_value=mock_powerview_userdata, - ), patch( - "homeassistant.components.hunterdouglas_powerview.async_setup", - return_value=True, - ) as mock_setup, patch( - "homeassistant.components.hunterdouglas_powerview.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={"host": "1.2.3.4"}, - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == "AlexanderHD" - assert result["data"] == { - "host": "1.2.3.4", - } - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - async def test_form_homekit(hass): """Test we get the form with homekit source.""" await setup.async_setup_component(hass, "persistent_notification", {}) diff --git a/tests/components/hvv_departures/test_config_flow.py b/tests/components/hvv_departures/test_config_flow.py index bd3e955d2d8..a6a927afd2e 100644 --- a/tests/components/hvv_departures/test_config_flow.py +++ b/tests/components/hvv_departures/test_config_flow.py @@ -1,5 +1,6 @@ """Test the HVV Departures config flow.""" import json +from unittest.mock import patch from pygti.exceptions import CannotConnect, InvalidAuth @@ -13,7 +14,6 @@ from homeassistant.components.hvv_departures.const import ( from homeassistant.config_entries import CONN_CLASS_CLOUD_POLL, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_OFFSET, CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture FIXTURE_INIT = json.loads(load_fixture("hvv_departures/init.json")) diff --git a/tests/components/hyperion/__init__.py b/tests/components/hyperion/__init__.py index 31a6c49eeb3..e427cf46a83 100644 --- a/tests/components/hyperion/__init__.py +++ b/tests/components/hyperion/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations import logging from types import TracebackType from typing import Any, Dict, Optional, Type +from unittest.mock import AsyncMock, Mock, patch from hyperion import const @@ -13,13 +14,13 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import AsyncMock, Mock, patch # type: ignore[attr-defined] from tests.common import MockConfigEntry TEST_HOST = "test" TEST_PORT = const.DEFAULT_PORT_JSON + 1 TEST_PORT_UI = const.DEFAULT_PORT_UI + 1 TEST_INSTANCE = 1 +TEST_ID = "default" TEST_SYSINFO_ID = "f9aab089-f85a-55cf-b7c1-222a72faebe9" TEST_SYSINFO_VERSION = "2.0.0-alpha.8" TEST_PRIORITY = 180 @@ -28,6 +29,7 @@ TEST_YAML_ENTITY_ID = f"{LIGHT_DOMAIN}.{TEST_YAML_NAME}" TEST_ENTITY_ID_1 = "light.test_instance_1" TEST_ENTITY_ID_2 = "light.test_instance_2" TEST_ENTITY_ID_3 = "light.test_instance_3" +TEST_PRIORITY_LIGHT_ENTITY_ID_1 = "light.test_instance_1_priority" TEST_TITLE = f"{TEST_HOST}:{TEST_PORT}" TEST_TOKEN = "sekr1t" @@ -67,7 +69,7 @@ TEST_AUTH_NOT_REQUIRED_RESP = { _LOGGER = logging.getLogger(__name__) -class AsyncContextManagerMock(Mock): # type: ignore[misc] +class AsyncContextManagerMock(Mock): """An async context manager mock for Hyperion.""" async def __aenter__(self) -> Optional[AsyncContextManagerMock]: @@ -111,6 +113,7 @@ def create_mock_client() -> Mock: } ) + mock_client.priorities = [] mock_client.adjustment = None mock_client.effects = None mock_client.instances = [ @@ -159,3 +162,12 @@ async def setup_test_config_entry( await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() return config_entry + + +def call_registered_callback( + client: AsyncMock, key: str, *args: Any, **kwargs: Any +) -> None: + """Call Hyperion entity callbacks that were registered with the client.""" + for call in client.add_callbacks.call_args_list: + if key in call[0][0]: + call[0][0][key](*args, **kwargs) diff --git a/tests/components/hyperion/test_config_flow.py b/tests/components/hyperion/test_config_flow.py index 481b7957849..776d5b3b25b 100644 --- a/tests/components/hyperion/test_config_flow.py +++ b/tests/components/hyperion/test_config_flow.py @@ -2,6 +2,7 @@ import logging from typing import Any, Dict, Optional +from unittest.mock import AsyncMock, patch from hyperion import const @@ -43,7 +44,6 @@ from . import ( create_mock_client, ) -from tests.async_mock import AsyncMock, patch # type: ignore[attr-defined] from tests.common import MockConfigEntry _LOGGER = logging.getLogger(__name__) @@ -689,6 +689,7 @@ async def test_options(hass: HomeAssistantType) -> None: {ATTR_ENTITY_ID: TEST_ENTITY_ID_1}, blocking=True, ) + # pylint: disable=unsubscriptable-object assert client.async_send_set_color.call_args[1][CONF_PRIORITY] == new_priority diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py index 4636a9ad59c..7559af4d3c7 100644 --- a/tests/components/hyperion/test_light.py +++ b/tests/components/hyperion/test_light.py @@ -1,7 +1,8 @@ """Tests for the Hyperion integration.""" import logging from types import MappingProxyType -from typing import Any, Optional +from typing import Optional +from unittest.mock import AsyncMock, Mock, call, patch from hyperion import const @@ -10,7 +11,11 @@ from homeassistant.components.hyperion import ( get_hyperion_unique_id, light as hyperion_light, ) -from homeassistant.components.hyperion.const import DOMAIN, TYPE_HYPERION_LIGHT +from homeassistant.components.hyperion.const import ( + DEFAULT_ORIGIN, + DOMAIN, + TYPE_HYPERION_LIGHT, +) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_EFFECT, @@ -33,6 +38,7 @@ from homeassistant.const import ( ) from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.helpers.typing import HomeAssistantType +import homeassistant.util.color as color_util from . import ( TEST_AUTH_NOT_REQUIRED_RESP, @@ -42,29 +48,25 @@ from . import ( TEST_ENTITY_ID_2, TEST_ENTITY_ID_3, TEST_HOST, + TEST_ID, TEST_INSTANCE_1, TEST_INSTANCE_2, TEST_INSTANCE_3, TEST_PORT, TEST_PRIORITY, + TEST_PRIORITY_LIGHT_ENTITY_ID_1, TEST_SYSINFO_ID, TEST_YAML_ENTITY_ID, TEST_YAML_NAME, add_test_config_entry, + call_registered_callback, create_mock_client, setup_test_config_entry, ) -from tests.async_mock import AsyncMock, call, patch # type: ignore[attr-defined] - _LOGGER = logging.getLogger(__name__) - -def _call_registered_callback( - client: AsyncMock, key: str, *args: Any, **kwargs: Any -) -> None: - """Call a Hyperion entity callback that was registered with the client.""" - client.set_callbacks.call_args[0][0][key](*args, **kwargs) +COLOR_BLACK = color_util.COLORS["black"] async def _setup_entity_yaml(hass: HomeAssistantType, client: AsyncMock = None) -> None: @@ -106,6 +108,7 @@ async def test_setup_yaml_already_converted(hass: HomeAssistantType) -> None: add_test_config_entry(hass) client = create_mock_client() await _setup_entity_yaml(hass, client=client) + assert client.async_client_disconnect.called # Setup should be skipped for the YAML config as there is a pre-existing config # entry. @@ -128,6 +131,7 @@ async def test_setup_yaml_old_style_unique_id(hass: HomeAssistantType) -> None: client = create_mock_client() await _setup_entity_yaml(hass, client=client) + assert client.async_client_disconnect.called # The entity should have been created with the same entity_id. assert hass.states.get(TEST_YAML_ENTITY_ID) is not None @@ -167,6 +171,7 @@ async def test_setup_yaml_new_style_unique_id_wo_config( client = create_mock_client() await _setup_entity_yaml(hass, client=client) + assert client.async_client_disconnect.called # The entity should have been created with the same entity_id. assert hass.states.get(entity_id_to_preserve) is not None @@ -190,6 +195,7 @@ async def test_setup_yaml_no_registry_entity(hass: HomeAssistantType) -> None: # Add a pre-existing config entry. client = create_mock_client() await _setup_entity_yaml(hass, client=client) + assert client.async_client_disconnect.called # The entity should have been created with the same entity_id. assert hass.states.get(TEST_YAML_ENTITY_ID) is not None @@ -211,6 +217,7 @@ async def test_setup_yaml_not_ready(hass: HomeAssistantType) -> None: client = create_mock_client() client.async_client_connect = AsyncMock(return_value=False) await _setup_entity_yaml(hass, client=client) + assert client.async_client_disconnect.called assert hass.states.get(TEST_YAML_ENTITY_ID) is None @@ -237,6 +244,7 @@ async def test_setup_config_entry_not_ready_switch_instance_fail( client = create_mock_client() client.async_client_switch_instance = AsyncMock(return_value=False) await setup_test_config_entry(hass, hyperion_client=client) + assert client.async_client_disconnect.called assert hass.states.get(TEST_ENTITY_ID_1) is None @@ -253,11 +261,14 @@ async def test_setup_config_entry_not_ready_load_state_fail( ) await setup_test_config_entry(hass, hyperion_client=client) + assert client.async_client_disconnect.called assert hass.states.get(TEST_ENTITY_ID_1) is None async def test_setup_config_entry_dynamic_instances(hass: HomeAssistantType) -> None: - """Test dynamic changes in the omstamce configuration.""" + """Test dynamic changes in the instance configuration.""" + registry = await async_get_registry(hass) + config_entry = add_test_config_entry(hass) master_client = create_mock_client() @@ -276,33 +287,18 @@ async def test_setup_config_entry_dynamic_instances(hass: HomeAssistantType) -> assert hass.states.get(TEST_ENTITY_ID_1) is not None assert hass.states.get(TEST_ENTITY_ID_2) is not None - # Inject a new instances update (remove instance 1, add instance 3) assert master_client.set_callbacks.called + + # == Inject a new instances update (stop instance 1, add instance 3) instance_callback = master_client.set_callbacks.call_args[0][0][ f"{const.KEY_INSTANCE}-{const.KEY_UPDATE}" ] + with patch( "homeassistant.components.hyperion.client.HyperionClient", return_value=entity_client, ): - instance_callback( - { - const.KEY_SUCCESS: True, - const.KEY_DATA: [TEST_INSTANCE_2, TEST_INSTANCE_3], - } - ) - await hass.async_block_till_done() - - assert hass.states.get(TEST_ENTITY_ID_1) is None - assert hass.states.get(TEST_ENTITY_ID_2) is not None - assert hass.states.get(TEST_ENTITY_ID_3) is not None - - # Inject a new instances update (re-add instance 1, but not running) - with patch( - "homeassistant.components.hyperion.client.HyperionClient", - return_value=entity_client, - ): - instance_callback( + await instance_callback( { const.KEY_SUCCESS: True, const.KEY_DATA: [ @@ -318,12 +314,60 @@ async def test_setup_config_entry_dynamic_instances(hass: HomeAssistantType) -> assert hass.states.get(TEST_ENTITY_ID_2) is not None assert hass.states.get(TEST_ENTITY_ID_3) is not None - # Inject a new instances update (re-add instance 1, running) + # Instance 1 is stopped, it should still be registered. + assert registry.async_is_registered(TEST_ENTITY_ID_1) + + # == Inject a new instances update (remove instance 1) + assert master_client.set_callbacks.called + instance_callback = master_client.set_callbacks.call_args[0][0][ + f"{const.KEY_INSTANCE}-{const.KEY_UPDATE}" + ] with patch( "homeassistant.components.hyperion.client.HyperionClient", return_value=entity_client, ): - instance_callback( + await instance_callback( + { + const.KEY_SUCCESS: True, + const.KEY_DATA: [TEST_INSTANCE_2, TEST_INSTANCE_3], + } + ) + await hass.async_block_till_done() + + assert hass.states.get(TEST_ENTITY_ID_1) is None + assert hass.states.get(TEST_ENTITY_ID_2) is not None + assert hass.states.get(TEST_ENTITY_ID_3) is not None + + # Instance 1 is removed, it should not still be registered. + assert not registry.async_is_registered(TEST_ENTITY_ID_1) + + # == Inject a new instances update (re-add instance 1, but not running) + with patch( + "homeassistant.components.hyperion.client.HyperionClient", + return_value=entity_client, + ): + await instance_callback( + { + const.KEY_SUCCESS: True, + const.KEY_DATA: [ + {**TEST_INSTANCE_1, "running": False}, + TEST_INSTANCE_2, + TEST_INSTANCE_3, + ], + } + ) + await hass.async_block_till_done() + + assert hass.states.get(TEST_ENTITY_ID_1) is None + assert hass.states.get(TEST_ENTITY_ID_2) is not None + assert hass.states.get(TEST_ENTITY_ID_3) is not None + + # == Inject a new instances update (re-add instance 1, running) + with patch( + "homeassistant.components.hyperion.client.HyperionClient", + return_value=entity_client, + ): + await instance_callback( { const.KEY_SUCCESS: True, const.KEY_DATA: [TEST_INSTANCE_1, TEST_INSTANCE_2, TEST_INSTANCE_3], @@ -372,7 +416,7 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None: **{ const.KEY_PRIORITY: TEST_PRIORITY, const.KEY_COLOR: [255, 255, 255], - const.KEY_ORIGIN: hyperion_light.DEFAULT_ORIGIN, + const.KEY_ORIGIN: DEFAULT_ORIGIN, } ) @@ -381,6 +425,7 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None: brightness = 128 client.async_send_set_color = AsyncMock(return_value=True) client.async_send_set_adjustment = AsyncMock(return_value=True) + client.adjustment = [{const.KEY_ID: TEST_ID}] await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, @@ -389,19 +434,29 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None: ) assert client.async_send_set_adjustment.call_args == call( - **{const.KEY_ADJUSTMENT: {const.KEY_BRIGHTNESS: 50}} + **{const.KEY_ADJUSTMENT: {const.KEY_BRIGHTNESS: 50, const.KEY_ID: TEST_ID}} ) assert client.async_send_set_color.call_args == call( **{ const.KEY_PRIORITY: TEST_PRIORITY, const.KEY_COLOR: [255, 255, 255], - const.KEY_ORIGIN: hyperion_light.DEFAULT_ORIGIN, + const.KEY_ORIGIN: DEFAULT_ORIGIN, } ) + # Simulate a false return of async_send_set_adjustment + client.async_send_set_adjustment = AsyncMock(return_value=False) + client.adjustment = [{const.KEY_ID: TEST_ID}] + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: TEST_ENTITY_ID_1, ATTR_BRIGHTNESS: brightness}, + blocking=True, + ) + # Simulate a state callback from Hyperion. client.adjustment = [{const.KEY_BRIGHTNESS: 50}] - _call_registered_callback(client, "adjustment-update") + call_registered_callback(client, "adjustment-update") entity_state = hass.states.get(TEST_ENTITY_ID_1) assert entity_state assert entity_state.state == "on" @@ -421,7 +476,7 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None: **{ const.KEY_PRIORITY: TEST_PRIORITY, const.KEY_COLOR: (0, 255, 255), - const.KEY_ORIGIN: hyperion_light.DEFAULT_ORIGIN, + const.KEY_ORIGIN: DEFAULT_ORIGIN, } ) @@ -431,7 +486,7 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None: const.KEY_VALUE: {const.KEY_RGB: (0, 255, 255)}, } - _call_registered_callback(client, "priorities-update") + call_registered_callback(client, "priorities-update") entity_state = hass.states.get(TEST_ENTITY_ID_1) assert entity_state assert entity_state.attributes["hs_color"] == hs_color @@ -441,6 +496,7 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None: brightness = 255 client.async_send_set_color = AsyncMock(return_value=True) client.async_send_set_adjustment = AsyncMock(return_value=True) + client.adjustment = [{const.KEY_ID: TEST_ID}] await hass.services.async_call( LIGHT_DOMAIN, @@ -450,17 +506,17 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None: ) assert client.async_send_set_adjustment.call_args == call( - **{const.KEY_ADJUSTMENT: {const.KEY_BRIGHTNESS: 100}} + **{const.KEY_ADJUSTMENT: {const.KEY_BRIGHTNESS: 100, const.KEY_ID: TEST_ID}} ) assert client.async_send_set_color.call_args == call( **{ const.KEY_PRIORITY: TEST_PRIORITY, const.KEY_COLOR: (0, 255, 255), - const.KEY_ORIGIN: hyperion_light.DEFAULT_ORIGIN, + const.KEY_ORIGIN: DEFAULT_ORIGIN, } ) client.adjustment = [{const.KEY_BRIGHTNESS: 100}] - _call_registered_callback(client, "adjustment-update") + call_registered_callback(client, "adjustment-update") entity_state = hass.states.get(TEST_ENTITY_ID_1) assert entity_state assert entity_state.attributes["brightness"] == brightness @@ -506,7 +562,7 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None: ), ] client.visible_priority = {const.KEY_COMPONENTID: effect} - _call_registered_callback(client, "priorities-update") + call_registered_callback(client, "priorities-update") entity_state = hass.states.get(TEST_ENTITY_ID_1) assert entity_state assert entity_state.attributes["icon"] == hyperion_light.ICON_EXTERNAL_SOURCE @@ -531,14 +587,14 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None: **{ const.KEY_PRIORITY: TEST_PRIORITY, const.KEY_EFFECT: {const.KEY_NAME: effect}, - const.KEY_ORIGIN: hyperion_light.DEFAULT_ORIGIN, + const.KEY_ORIGIN: DEFAULT_ORIGIN, } ) client.visible_priority = { const.KEY_COMPONENTID: const.KEY_COMPONENTID_EFFECT, const.KEY_OWNER: effect, } - _call_registered_callback(client, "priorities-update") + call_registered_callback(client, "priorities-update") entity_state = hass.states.get(TEST_ENTITY_ID_1) assert entity_state assert entity_state.attributes["icon"] == hyperion_light.ICON_EFFECT @@ -559,7 +615,7 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None: **{ const.KEY_PRIORITY: TEST_PRIORITY, const.KEY_COLOR: (0, 0, 255), - const.KEY_ORIGIN: hyperion_light.DEFAULT_ORIGIN, + const.KEY_ORIGIN: DEFAULT_ORIGIN, } ) # Simulate a state callback from Hyperion. @@ -567,7 +623,7 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None: const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR, const.KEY_VALUE: {const.KEY_RGB: (0, 0, 255)}, } - _call_registered_callback(client, "priorities-update") + call_registered_callback(client, "priorities-update") entity_state = hass.states.get(TEST_ENTITY_ID_1) assert entity_state assert entity_state.attributes["hs_color"] == hs_color @@ -576,7 +632,7 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None: # No calls if disconnected. client.has_loaded_state = False - _call_registered_callback(client, "client-update", {"loaded-state": False}) + call_registered_callback(client, "client-update", {"loaded-state": False}) client.async_send_clear = AsyncMock(return_value=True) client.async_send_set_effect = AsyncMock(return_value=True) @@ -588,6 +644,51 @@ async def test_light_async_turn_on(hass: HomeAssistantType) -> None: assert not client.async_send_set_effect.called +async def test_light_async_turn_on_error_conditions(hass: HomeAssistantType) -> None: + """Test error conditions when turning the light on.""" + client = create_mock_client() + client.async_send_set_component = AsyncMock(return_value=False) + client.is_on = Mock(return_value=False) + await setup_test_config_entry(hass, hyperion_client=client) + + # On (=), 100% (=), solid (=), [255,255,255] (=) + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: TEST_ENTITY_ID_1}, blocking=True + ) + + assert client.async_send_set_component.call_args == call( + **{ + const.KEY_COMPONENTSTATE: { + const.KEY_COMPONENT: const.KEY_COMPONENTID_ALL, + const.KEY_STATE: True, + } + } + ) + + +async def test_light_async_turn_off_error_conditions(hass: HomeAssistantType) -> None: + """Test error conditions when turning the light off.""" + client = create_mock_client() + client.async_send_set_component = AsyncMock(return_value=False) + await setup_test_config_entry(hass, hyperion_client=client) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: TEST_ENTITY_ID_1}, + blocking=True, + ) + + assert client.async_send_set_component.call_args == call( + **{ + const.KEY_COMPONENTSTATE: { + const.KEY_COMPONENT: const.KEY_COMPONENTID_LEDDEVICE, + const.KEY_STATE: False, + } + } + ) + + async def test_light_async_turn_off(hass: HomeAssistantType) -> None: """Test turning the light off.""" client = create_mock_client() @@ -610,10 +711,15 @@ async def test_light_async_turn_off(hass: HomeAssistantType) -> None: } ) + call_registered_callback(client, "components-update") + entity_state = hass.states.get(TEST_ENTITY_ID_1) + assert entity_state + assert entity_state.attributes["icon"] == hyperion_light.ICON_LIGHTBULB + # No calls if no state loaded. client.has_loaded_state = False client.async_send_set_component = AsyncMock(return_value=True) - _call_registered_callback(client, "client-update", {"loaded-state": False}) + call_registered_callback(client, "client-update", {"loaded-state": False}) await hass.services.async_call( LIGHT_DOMAIN, @@ -635,7 +741,7 @@ async def test_light_async_updates_from_hyperion_client( # Bright change gets accepted. brightness = 10 client.adjustment = [{const.KEY_BRIGHTNESS: brightness}] - _call_registered_callback(client, "adjustment-update") + call_registered_callback(client, "adjustment-update") entity_state = hass.states.get(TEST_ENTITY_ID_1) assert entity_state assert entity_state.attributes["brightness"] == round(255 * (brightness / 100.0)) @@ -643,20 +749,20 @@ async def test_light_async_updates_from_hyperion_client( # Broken brightness value is ignored. bad_brightness = -200 client.adjustment = [{const.KEY_BRIGHTNESS: bad_brightness}] - _call_registered_callback(client, "adjustment-update") + call_registered_callback(client, "adjustment-update") entity_state = hass.states.get(TEST_ENTITY_ID_1) assert entity_state assert entity_state.attributes["brightness"] == round(255 * (brightness / 100.0)) # Update components. client.is_on.return_value = True - _call_registered_callback(client, "components-update") + call_registered_callback(client, "components-update") entity_state = hass.states.get(TEST_ENTITY_ID_1) assert entity_state assert entity_state.state == "on" client.is_on.return_value = False - _call_registered_callback(client, "components-update") + call_registered_callback(client, "components-update") entity_state = hass.states.get(TEST_ENTITY_ID_1) assert entity_state assert entity_state.state == "off" @@ -664,7 +770,7 @@ async def test_light_async_updates_from_hyperion_client( # Update priorities (V4L) client.is_on.return_value = True client.visible_priority = {const.KEY_COMPONENTID: const.KEY_COMPONENTID_V4L} - _call_registered_callback(client, "priorities-update") + call_registered_callback(client, "priorities-update") entity_state = hass.states.get(TEST_ENTITY_ID_1) assert entity_state assert entity_state.attributes["icon"] == hyperion_light.ICON_EXTERNAL_SOURCE @@ -678,7 +784,7 @@ async def test_light_async_updates_from_hyperion_client( const.KEY_OWNER: effect, } - _call_registered_callback(client, "priorities-update") + call_registered_callback(client, "priorities-update") entity_state = hass.states.get(TEST_ENTITY_ID_1) assert entity_state assert entity_state.attributes["effect"] == effect @@ -692,7 +798,7 @@ async def test_light_async_updates_from_hyperion_client( const.KEY_VALUE: {const.KEY_RGB: rgb}, } - _call_registered_callback(client, "priorities-update") + call_registered_callback(client, "priorities-update") entity_state = hass.states.get(TEST_ENTITY_ID_1) assert entity_state assert entity_state.attributes["effect"] == hyperion_light.KEY_EFFECT_SOLID @@ -702,7 +808,7 @@ async def test_light_async_updates_from_hyperion_client( # Update priorities (None) client.visible_priority = None - _call_registered_callback(client, "priorities-update") + call_registered_callback(client, "priorities-update") entity_state = hass.states.get(TEST_ENTITY_ID_1) assert entity_state assert entity_state.state == "off" @@ -710,18 +816,20 @@ async def test_light_async_updates_from_hyperion_client( # Update effect list effects = [{const.KEY_NAME: "One"}, {const.KEY_NAME: "Two"}] client.effects = effects - _call_registered_callback(client, "effects-update") + call_registered_callback(client, "effects-update") entity_state = hass.states.get(TEST_ENTITY_ID_1) assert entity_state assert entity_state.attributes["effect_list"] == [ + hyperion_light.KEY_EFFECT_SOLID + ] + const.KEY_COMPONENTID_EXTERNAL_SOURCES + [ effect[const.KEY_NAME] for effect in effects - ] + const.KEY_COMPONENTID_EXTERNAL_SOURCES + [hyperion_light.KEY_EFFECT_SOLID] + ] # Update connection status (e.g. disconnection). # Turn on late, check state, disconnect, ensure it cannot be turned off. client.has_loaded_state = False - _call_registered_callback(client, "client-update", {"loaded-state": False}) + call_registered_callback(client, "client-update", {"loaded-state": False}) entity_state = hass.states.get(TEST_ENTITY_ID_1) assert entity_state assert entity_state.state == "unavailable" @@ -732,7 +840,7 @@ async def test_light_async_updates_from_hyperion_client( const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR, const.KEY_VALUE: {const.KEY_RGB: rgb}, } - _call_registered_callback(client, "client-update", {"loaded-state": True}) + call_registered_callback(client, "client-update", {"loaded-state": True}) entity_state = hass.states.get(TEST_ENTITY_ID_1) assert entity_state assert entity_state.state == "on" @@ -803,6 +911,7 @@ async def test_setup_entry_no_token_reauth(hass: HomeAssistantType) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: assert not await hass.config_entries.async_setup(config_entry.entry_id) + assert client.async_client_disconnect.called mock_flow_init.assert_called_once_with( DOMAIN, context={CONF_SOURCE: SOURCE_REAUTH}, @@ -826,9 +935,354 @@ async def test_setup_entry_bad_token_reauth(hass: HomeAssistantType) -> None: "homeassistant.components.hyperion.client.HyperionClient", return_value=client ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: assert not await hass.config_entries.async_setup(config_entry.entry_id) + assert client.async_client_disconnect.called mock_flow_init.assert_called_once_with( DOMAIN, context={CONF_SOURCE: SOURCE_REAUTH}, data=config_entry.data, ) assert config_entry.state == ENTRY_STATE_SETUP_ERROR + + +async def test_priority_light_async_updates( + hass: HomeAssistantType, +) -> None: + """Test receiving a variety of Hyperion client callbacks to a HyperionPriorityLight.""" + priority_template = { + const.KEY_ACTIVE: True, + const.KEY_VISIBLE: True, + const.KEY_PRIORITY: TEST_PRIORITY, + const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR, + const.KEY_VALUE: {const.KEY_RGB: (100, 100, 100)}, + } + + client = create_mock_client() + client.priorities = [{**priority_template}] + + with patch( + "homeassistant.components.hyperion.light.HyperionPriorityLight.entity_registry_enabled_default" + ) as enabled_by_default_mock: + enabled_by_default_mock.return_value = True + await setup_test_config_entry(hass, hyperion_client=client) + + # == Scenario: Color at HA priority will show light as on. + entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1) + assert entity_state + assert entity_state.state == "on" + assert entity_state.attributes["hs_color"] == (0.0, 0.0) + + # == Scenario: Color going to black shows the light as off. + client.priorities = [ + { + **priority_template, + const.KEY_VALUE: {const.KEY_RGB: COLOR_BLACK}, + } + ] + client.visible_priority = client.priorities[0] + + call_registered_callback(client, "priorities-update") + entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1) + assert entity_state + assert entity_state.state == "off" + + # == Scenario: Lower priority than HA priority should have no impact on what HA + # shows when the HA priority is present. + client.priorities = [ + {**priority_template, const.KEY_PRIORITY: TEST_PRIORITY - 1}, + { + **priority_template, + const.KEY_VALUE: {const.KEY_RGB: COLOR_BLACK}, + }, + ] + client.visible_priority = client.priorities[0] + + call_registered_callback(client, "priorities-update") + entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1) + assert entity_state + assert entity_state.state == "off" + + # == Scenario: Fresh color at HA priority should turn HA entity on (even though + # there's a lower priority enabled/visible in Hyperion). + client.priorities = [ + {**priority_template, const.KEY_PRIORITY: TEST_PRIORITY - 1}, + { + **priority_template, + const.KEY_PRIORITY: TEST_PRIORITY, + const.KEY_VALUE: {const.KEY_RGB: (100, 100, 150)}, + }, + ] + client.visible_priority = client.priorities[0] + + call_registered_callback(client, "priorities-update") + entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1) + assert entity_state + assert entity_state.state == "on" + assert entity_state.attributes["hs_color"] == (240.0, 33.333) + + # == Scenario: V4L at a higher priority, with no other HA priority at all, should + # have no effect. + + # Emulate HA turning the light off with black at the HA priority. + client.priorities = [] + client.visible_priority = None + + call_registered_callback(client, "priorities-update") + entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1) + assert entity_state + assert entity_state.state == "off" + + # Emulate V4L turning on. + client.priorities = [ + { + **priority_template, + const.KEY_PRIORITY: 240, + const.KEY_COMPONENTID: const.KEY_COMPONENTID_V4L, + const.KEY_VALUE: {const.KEY_RGB: (100, 100, 150)}, + }, + ] + client.visible_priority = client.priorities[0] + + call_registered_callback(client, "priorities-update") + entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1) + assert entity_state + assert entity_state.state == "off" + + # == Scenario: A lower priority input (lower priority than HA) should have no effect. + + client.priorities = [ + { + **priority_template, + const.KEY_VISIBLE: True, + const.KEY_PRIORITY: TEST_PRIORITY - 1, + const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR, + const.KEY_VALUE: {const.KEY_RGB: (255, 0, 0)}, + }, + { + **priority_template, + const.KEY_PRIORITY: 240, + const.KEY_COMPONENTID: const.KEY_COMPONENTID_V4L, + const.KEY_VALUE: {const.KEY_RGB: (100, 100, 150)}, + const.KEY_VISIBLE: False, + }, + ] + + client.visible_priority = client.priorities[0] + call_registered_callback(client, "priorities-update") + entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1) + assert entity_state + assert entity_state.state == "off" + + # == Scenario: A non-active priority is ignored. + client.priorities = [ + { + const.KEY_ACTIVE: False, + const.KEY_VISIBLE: False, + const.KEY_PRIORITY: TEST_PRIORITY, + const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR, + const.KEY_VALUE: {const.KEY_RGB: (100, 100, 100)}, + } + ] + client.visible_priority = None + call_registered_callback(client, "priorities-update") + entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1) + assert entity_state + assert entity_state.state == "off" + + # == Scenario: A priority with no ... priority ... is ignored. + client.priorities = [ + { + const.KEY_ACTIVE: True, + const.KEY_VISIBLE: True, + const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR, + const.KEY_VALUE: {const.KEY_RGB: (100, 100, 100)}, + } + ] + client.visible_priority = None + call_registered_callback(client, "priorities-update") + entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1) + assert entity_state + assert entity_state.state == "off" + + +async def test_priority_light_async_updates_off_sets_black( + hass: HomeAssistantType, +) -> None: + """Test turning the HyperionPriorityLight off.""" + client = create_mock_client() + client.priorities = [ + { + const.KEY_ACTIVE: True, + const.KEY_VISIBLE: True, + const.KEY_PRIORITY: TEST_PRIORITY, + const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR, + const.KEY_VALUE: {const.KEY_RGB: (100, 100, 100)}, + } + ] + + with patch( + "homeassistant.components.hyperion.light.HyperionPriorityLight.entity_registry_enabled_default" + ) as enabled_by_default_mock: + enabled_by_default_mock.return_value = True + await setup_test_config_entry(hass, hyperion_client=client) + + client.async_send_clear = AsyncMock(return_value=True) + client.async_send_set_color = AsyncMock(return_value=True) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: TEST_PRIORITY_LIGHT_ENTITY_ID_1}, + blocking=True, + ) + + assert client.async_send_clear.call_args == call( + **{ + const.KEY_PRIORITY: TEST_PRIORITY, + } + ) + + assert client.async_send_set_color.call_args == call( + **{ + const.KEY_PRIORITY: TEST_PRIORITY, + const.KEY_COLOR: COLOR_BLACK, + const.KEY_ORIGIN: DEFAULT_ORIGIN, + } + ) + + +async def test_priority_light_prior_color_preserved_after_black( + hass: HomeAssistantType, +) -> None: + """Test that color is preserved in an on->off->on cycle for a HyperionPriorityLight. + + For a HyperionPriorityLight the color black is used to indicate off. This test + ensures that a cycle through 'off' will preserve the original color. + """ + priority_template = { + const.KEY_ACTIVE: True, + const.KEY_VISIBLE: True, + const.KEY_PRIORITY: TEST_PRIORITY, + const.KEY_COMPONENTID: const.KEY_COMPONENTID_COLOR, + } + + client = create_mock_client() + client.async_send_set_color = AsyncMock(return_value=True) + client.async_send_clear = AsyncMock(return_value=True) + client.priorities = [] + client.visible_priority = None + + with patch( + "homeassistant.components.hyperion.light.HyperionPriorityLight.entity_registry_enabled_default" + ) as enabled_by_default_mock: + enabled_by_default_mock.return_value = True + await setup_test_config_entry(hass, hyperion_client=client) + + # Turn the light on full green... + # On (=), 100% (=), solid (=), [0,0,255] (=) + hs_color = (240.0, 100.0) + rgb_color = (0, 0, 255) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: TEST_PRIORITY_LIGHT_ENTITY_ID_1, ATTR_HS_COLOR: hs_color}, + blocking=True, + ) + + assert client.async_send_set_color.call_args == call( + **{ + const.KEY_PRIORITY: TEST_PRIORITY, + const.KEY_COLOR: rgb_color, + const.KEY_ORIGIN: DEFAULT_ORIGIN, + } + ) + + client.priorities = [ + { + **priority_template, + const.KEY_VALUE: {const.KEY_RGB: rgb_color}, + } + ] + client.visible_priority = client.priorities[0] + call_registered_callback(client, "priorities-update") + + entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1) + assert entity_state + assert entity_state.state == "on" + assert entity_state.attributes["hs_color"] == hs_color + + # Then turn it off. + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: TEST_PRIORITY_LIGHT_ENTITY_ID_1}, + blocking=True, + ) + + assert client.async_send_set_color.call_args == call( + **{ + const.KEY_PRIORITY: TEST_PRIORITY, + const.KEY_COLOR: COLOR_BLACK, + const.KEY_ORIGIN: DEFAULT_ORIGIN, + } + ) + + client.priorities = [ + { + **priority_template, + const.KEY_VALUE: {const.KEY_RGB: COLOR_BLACK}, + } + ] + client.visible_priority = client.priorities[0] + call_registered_callback(client, "priorities-update") + + entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1) + assert entity_state + assert entity_state.state == "off" + + # Then turn it back on and ensure it's still green. + # On (=), 100% (=), solid (=), [0,0,255] (=) + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: TEST_PRIORITY_LIGHT_ENTITY_ID_1}, + blocking=True, + ) + + assert client.async_send_set_color.call_args == call( + **{ + const.KEY_PRIORITY: TEST_PRIORITY, + const.KEY_COLOR: rgb_color, + const.KEY_ORIGIN: DEFAULT_ORIGIN, + } + ) + + client.priorities = [ + { + **priority_template, + const.KEY_VALUE: {const.KEY_RGB: rgb_color}, + } + ] + client.visible_priority = client.priorities[0] + call_registered_callback(client, "priorities-update") + + entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1) + assert entity_state + assert entity_state.state == "on" + assert entity_state.attributes["hs_color"] == hs_color + + +async def test_priority_light_has_no_external_sources(hass: HomeAssistantType) -> None: + """Ensure a HyperionPriorityLight does not list external sources.""" + client = create_mock_client() + client.priorities = [] + + with patch( + "homeassistant.components.hyperion.light.HyperionPriorityLight.entity_registry_enabled_default" + ) as enabled_by_default_mock: + enabled_by_default_mock.return_value = True + await setup_test_config_entry(hass, hyperion_client=client) + + entity_state = hass.states.get(TEST_PRIORITY_LIGHT_ENTITY_ID_1) + assert entity_state + assert entity_state.attributes["effect_list"] == [hyperion_light.KEY_EFFECT_SOLID] diff --git a/tests/components/hyperion/test_switch.py b/tests/components/hyperion/test_switch.py new file mode 100644 index 00000000000..dcfba9662bf --- /dev/null +++ b/tests/components/hyperion/test_switch.py @@ -0,0 +1,140 @@ +"""Tests for the Hyperion integration.""" +import logging +from unittest.mock import AsyncMock, call, patch + +from hyperion.const import ( + KEY_COMPONENT, + KEY_COMPONENTID_ALL, + KEY_COMPONENTID_BLACKBORDER, + KEY_COMPONENTID_BOBLIGHTSERVER, + KEY_COMPONENTID_FORWARDER, + KEY_COMPONENTID_GRABBER, + KEY_COMPONENTID_LEDDEVICE, + KEY_COMPONENTID_SMOOTHING, + KEY_COMPONENTID_V4L, + KEY_COMPONENTSTATE, + KEY_STATE, +) + +from homeassistant.components.hyperion.const import COMPONENT_TO_NAME +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util import slugify + +from . import call_registered_callback, create_mock_client, setup_test_config_entry + +TEST_COMPONENTS = [ + {"enabled": True, "name": "ALL"}, + {"enabled": True, "name": "SMOOTHING"}, + {"enabled": True, "name": "BLACKBORDER"}, + {"enabled": False, "name": "FORWARDER"}, + {"enabled": False, "name": "BOBLIGHTSERVER"}, + {"enabled": False, "name": "GRABBER"}, + {"enabled": False, "name": "V4L"}, + {"enabled": True, "name": "LEDDEVICE"}, +] + +_LOGGER = logging.getLogger(__name__) +TEST_SWITCH_COMPONENT_BASE_ENTITY_ID = "switch.test_instance_1_component" +TEST_SWITCH_COMPONENT_ALL_ENTITY_ID = f"{TEST_SWITCH_COMPONENT_BASE_ENTITY_ID}_all" + + +async def test_switch_turn_on_off(hass: HomeAssistantType) -> None: + """Test turning the light on.""" + client = create_mock_client() + client.async_send_set_component = AsyncMock(return_value=True) + client.components = TEST_COMPONENTS + + # Setup component switch. + with patch( + "homeassistant.components.hyperion.switch.HyperionComponentSwitch.entity_registry_enabled_default" + ) as enabled_by_default_mock: + enabled_by_default_mock.return_value = True + await setup_test_config_entry(hass, hyperion_client=client) + + # Verify switch is on (as per TEST_COMPONENTS above). + entity_state = hass.states.get(TEST_SWITCH_COMPONENT_ALL_ENTITY_ID) + assert entity_state + assert entity_state.state == "on" + + # Turn switch off. + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: TEST_SWITCH_COMPONENT_ALL_ENTITY_ID}, + blocking=True, + ) + + # Verify correct parameters are passed to the library. + assert client.async_send_set_component.call_args == call( + **{KEY_COMPONENTSTATE: {KEY_COMPONENT: KEY_COMPONENTID_ALL, KEY_STATE: False}} + ) + + client.components[0] = { + "enabled": False, + "name": "ALL", + } + call_registered_callback(client, "components-update") + + # Verify the switch turns off. + entity_state = hass.states.get(TEST_SWITCH_COMPONENT_ALL_ENTITY_ID) + assert entity_state + assert entity_state.state == "off" + + # Turn switch on. + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: TEST_SWITCH_COMPONENT_ALL_ENTITY_ID}, + blocking=True, + ) + + # Verify correct parameters are passed to the library. + assert client.async_send_set_component.call_args == call( + **{KEY_COMPONENTSTATE: {KEY_COMPONENT: KEY_COMPONENTID_ALL, KEY_STATE: True}} + ) + + client.components[0] = { + "enabled": True, + "name": "ALL", + } + call_registered_callback(client, "components-update") + + # Verify the switch turns on. + entity_state = hass.states.get(TEST_SWITCH_COMPONENT_ALL_ENTITY_ID) + assert entity_state + assert entity_state.state == "on" + + +async def test_switch_has_correct_entities(hass: HomeAssistantType) -> None: + """Test that the correct switch entities are created.""" + client = create_mock_client() + client.components = TEST_COMPONENTS + + # Setup component switch. + with patch( + "homeassistant.components.hyperion.switch.HyperionComponentSwitch.entity_registry_enabled_default" + ) as enabled_by_default_mock: + enabled_by_default_mock.return_value = True + await setup_test_config_entry(hass, hyperion_client=client) + + entity_state = hass.states.get(TEST_SWITCH_COMPONENT_ALL_ENTITY_ID) + + for component in ( + KEY_COMPONENTID_ALL, + KEY_COMPONENTID_SMOOTHING, + KEY_COMPONENTID_BLACKBORDER, + KEY_COMPONENTID_FORWARDER, + KEY_COMPONENTID_BOBLIGHTSERVER, + KEY_COMPONENTID_GRABBER, + KEY_COMPONENTID_LEDDEVICE, + KEY_COMPONENTID_V4L, + ): + entity_id = ( + TEST_SWITCH_COMPONENT_BASE_ENTITY_ID + + "_" + + slugify(COMPONENT_TO_NAME[component]) + ) + entity_state = hass.states.get(entity_id) + assert entity_state, f"Couldn't find entity: {entity_id}" diff --git a/tests/components/icloud/conftest.py b/tests/components/icloud/conftest.py index 2ed9006cdb6..2230cc2ea32 100644 --- a/tests/components/icloud/conftest.py +++ b/tests/components/icloud/conftest.py @@ -1,7 +1,7 @@ """Configure iCloud tests.""" -import pytest +from unittest.mock import patch -from tests.async_mock import patch +import pytest @pytest.fixture(name="icloud_bypass_setup", autouse=True) diff --git a/tests/components/icloud/test_config_flow.py b/tests/components/icloud/test_config_flow.py index 5ed46a6136f..a774e61f3ec 100644 --- a/tests/components/icloud/test_config_flow.py +++ b/tests/components/icloud/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for the iCloud config flow.""" +from unittest.mock import MagicMock, Mock, patch + from pyicloud.exceptions import PyiCloudFailedLoginException import pytest @@ -20,7 +22,6 @@ from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, SOURCE_US from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import MagicMock, Mock, patch from tests.common import MockConfigEntry USERNAME = "username@me.com" diff --git a/tests/components/ifttt/test_init.py b/tests/components/ifttt/test_init.py index e5fd8841a2b..d10df2492d4 100644 --- a/tests/components/ifttt/test_init.py +++ b/tests/components/ifttt/test_init.py @@ -1,10 +1,10 @@ """Test the init file of IFTTT.""" +from unittest.mock import patch + from homeassistant import data_entry_flow from homeassistant.components import ifttt from homeassistant.core import callback -from tests.async_mock import patch - async def test_config_flow_registers_webhook(hass, aiohttp_client): """Test setting up IFTTT and sending webhook.""" diff --git a/tests/components/ign_sismologia/test_geo_location.py b/tests/components/ign_sismologia/test_geo_location.py index 5695f26c5aa..1ad5ae2a2b2 100644 --- a/tests/components/ign_sismologia/test_geo_location.py +++ b/tests/components/ign_sismologia/test_geo_location.py @@ -1,5 +1,6 @@ """The tests for the IGN Sismologia (Earthquakes) Feed platform.""" import datetime +from unittest.mock import MagicMock, call, patch from homeassistant.components import geo_location from homeassistant.components.geo_location import ATTR_SOURCE @@ -28,7 +29,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import MagicMock, call, patch from tests.common import assert_setup_component, async_fire_time_changed CONFIG = {geo_location.DOMAIN: [{"platform": "ign_sismologia", CONF_RADIUS: 200}]} diff --git a/tests/components/image/test_init.py b/tests/components/image/test_init.py index 277e6f07149..ab73bb71286 100644 --- a/tests/components/image/test_init.py +++ b/tests/components/image/test_init.py @@ -1,6 +1,7 @@ """Test that we can upload images.""" import pathlib import tempfile +from unittest.mock import patch from aiohttp import ClientSession, ClientWebSocketResponse @@ -8,8 +9,6 @@ from homeassistant.components.websocket_api import const as ws_const from homeassistant.setup import async_setup_component from homeassistant.util import dt as util_dt -from tests.async_mock import patch - async def test_upload_image(hass, hass_client, hass_ws_client): """Test we can upload an image.""" diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index c006a262e85..3e6a3cc960f 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -1,4 +1,6 @@ """The tests for the image_processing component.""" +from unittest.mock import PropertyMock, patch + import homeassistant.components.http as http import homeassistant.components.image_processing as ip from homeassistant.const import ATTR_ENTITY_PICTURE @@ -6,7 +8,6 @@ from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import setup_component -from tests.async_mock import PropertyMock, patch from tests.common import ( assert_setup_component, get_test_home_assistant, diff --git a/tests/components/influxdb/test_init.py b/tests/components/influxdb/test_init.py index da405e3975b..db22b5c5236 100644 --- a/tests/components/influxdb/test_init.py +++ b/tests/components/influxdb/test_init.py @@ -1,6 +1,7 @@ """The tests for the InfluxDB component.""" from dataclasses import dataclass import datetime +from unittest.mock import MagicMock, Mock, call, patch import pytest @@ -16,8 +17,6 @@ from homeassistant.const import ( from homeassistant.core import split_entity_id from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, Mock, call, patch - INFLUX_PATH = "homeassistant.components.influxdb" INFLUX_CLIENT_PATH = f"{INFLUX_PATH}.InfluxDBClient" BASE_V1_CONFIG = {} diff --git a/tests/components/influxdb/test_sensor.py b/tests/components/influxdb/test_sensor.py index 633f9e891f8..57983d14aba 100644 --- a/tests/components/influxdb/test_sensor.py +++ b/tests/components/influxdb/test_sensor.py @@ -2,6 +2,7 @@ from dataclasses import dataclass from datetime import timedelta from typing import Dict, List, Type +from unittest.mock import MagicMock, patch from influxdb.exceptions import InfluxDBClientError, InfluxDBServerError from influxdb_client.rest import ApiException @@ -24,7 +25,6 @@ from homeassistant.helpers.entity_platform import PLATFORM_NOT_READY_BASE_WAIT_T from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from tests.async_mock import MagicMock, patch from tests.common import async_fire_time_changed INFLUXDB_PATH = "homeassistant.components.influxdb" diff --git a/tests/components/input_boolean/test_init.py b/tests/components/input_boolean/test_init.py index 9911ced28dd..88562678436 100644 --- a/tests/components/input_boolean/test_init.py +++ b/tests/components/input_boolean/test_init.py @@ -1,6 +1,7 @@ """The tests for the input_boolean component.""" # pylint: disable=protected-access import logging +from unittest.mock import patch import pytest @@ -22,7 +23,6 @@ from homeassistant.core import Context, CoreState, State from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import mock_component, mock_restore_cache _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/input_datetime/test_init.py b/tests/components/input_datetime/test_init.py index a336ef82363..8d9ddf9546d 100644 --- a/tests/components/input_datetime/test_init.py +++ b/tests/components/input_datetime/test_init.py @@ -1,6 +1,7 @@ """Tests for the Input slider component.""" # pylint: disable=protected-access import datetime +from unittest.mock import patch import pytest import voluptuous as vol @@ -31,7 +32,6 @@ from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from tests.async_mock import patch from tests.common import mock_restore_cache INITIAL_DATE = "2020-01-10" @@ -131,7 +131,7 @@ async def test_set_datetime(hass): entity_id = "input_datetime.test_datetime" - dt_obj = datetime.datetime(2017, 9, 7, 19, 46, 30) + dt_obj = datetime.datetime(2017, 9, 7, 19, 46, 30, tzinfo=datetime.timezone.utc) await async_set_date_and_time(hass, entity_id, dt_obj) @@ -157,7 +157,7 @@ async def test_set_datetime_2(hass): entity_id = "input_datetime.test_datetime" - dt_obj = datetime.datetime(2017, 9, 7, 19, 46, 30) + dt_obj = datetime.datetime(2017, 9, 7, 19, 46, 30, tzinfo=datetime.timezone.utc) await async_set_datetime(hass, entity_id, dt_obj) @@ -183,7 +183,7 @@ async def test_set_datetime_3(hass): entity_id = "input_datetime.test_datetime" - dt_obj = datetime.datetime(2017, 9, 7, 19, 46, 30) + dt_obj = datetime.datetime(2017, 9, 7, 19, 46, 30, tzinfo=datetime.timezone.utc) await async_set_timestamp(hass, entity_id, dt_util.as_utc(dt_obj).timestamp()) @@ -320,7 +320,7 @@ async def test_restore_state(hass): hass.state = CoreState.starting initial = datetime.datetime(2017, 1, 1, 23, 42) - default = datetime.datetime(1970, 1, 1, 0, 0) + default = datetime.datetime.combine(datetime.date.today(), DEFAULT_TIME) await async_setup_component( hass, @@ -375,7 +375,7 @@ async def test_default_value(hass): }, ) - dt_obj = datetime.datetime(1970, 1, 1, 0, 0) + dt_obj = datetime.datetime.combine(datetime.date.today(), DEFAULT_TIME) state_time = hass.states.get("input_datetime.test_time") assert state_time.state == dt_obj.strftime(FMT_TIME) assert state_time.attributes.get("timestamp") is not None @@ -477,7 +477,9 @@ async def test_reload(hass, hass_admin_user, hass_read_only_user): assert state_2 is not None assert state_3 is None assert state_1.state == DEFAULT_TIME.strftime(FMT_TIME) - assert state_2.state == datetime.datetime(1970, 1, 1, 0, 0).strftime(FMT_DATETIME) + assert state_2.state == datetime.datetime.combine( + datetime.date.today(), DEFAULT_TIME + ).strftime(FMT_DATETIME) assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "dt1") == f"{DOMAIN}.dt1" assert ent_reg.async_get_entity_id(DOMAIN, DOMAIN, "dt2") == f"{DOMAIN}.dt2" @@ -677,6 +679,7 @@ async def test_timestamp(hass): # initial has been converted to the set timezone state_with_tz = hass.states.get("input_datetime.test_datetime_initial_with_tz") assert state_with_tz is not None + # Timezone LA is UTC-8 => timestamp carries +01:00 => delta is -9 => 10:00 - 09:00 => 01:00 assert state_with_tz.state == "2020-12-13 01:00:00" assert ( dt_util.as_local( @@ -691,6 +694,13 @@ async def test_timestamp(hass): ) assert state_without_tz is not None assert state_without_tz.state == "2020-12-13 10:00:00" + # Timezone LA is UTC-8 => timestamp has no zone (= assumed local) => delta to UTC is +8 => 10:00 + 08:00 => 18:00 + assert ( + dt_util.utc_from_timestamp( + state_without_tz.attributes[ATTR_TIMESTAMP] + ).strftime(FMT_DATETIME) + == "2020-12-13 18:00:00" + ) assert ( dt_util.as_local( dt_util.utc_from_timestamp(state_without_tz.attributes[ATTR_TIMESTAMP]) @@ -701,7 +711,7 @@ async def test_timestamp(hass): assert ( dt_util.as_local( datetime.datetime.fromtimestamp( - state_without_tz.attributes[ATTR_TIMESTAMP] + state_without_tz.attributes[ATTR_TIMESTAMP], datetime.timezone.utc ) ).strftime(FMT_DATETIME) == "2020-12-13 10:00:00" diff --git a/tests/components/input_number/test_init.py b/tests/components/input_number/test_init.py index 8971439de74..28b9d27d23f 100644 --- a/tests/components/input_number/test_init.py +++ b/tests/components/input_number/test_init.py @@ -1,4 +1,7 @@ """The tests for the Input number component.""" +# pylint: disable=protected-access +from unittest.mock import patch + import pytest import voluptuous as vol @@ -21,8 +24,6 @@ from homeassistant.exceptions import Unauthorized from homeassistant.helpers import entity_registry from homeassistant.setup import async_setup_component -# pylint: disable=protected-access -from tests.async_mock import patch from tests.common import mock_restore_cache diff --git a/tests/components/input_select/test_init.py b/tests/components/input_select/test_init.py index 6f83de340f3..38a3c3ba7a2 100644 --- a/tests/components/input_select/test_init.py +++ b/tests/components/input_select/test_init.py @@ -1,4 +1,7 @@ """The tests for the Input select component.""" +# pylint: disable=protected-access +from unittest.mock import patch + import pytest from homeassistant.components.input_select import ( @@ -6,6 +9,8 @@ from homeassistant.components.input_select import ( ATTR_OPTIONS, CONF_INITIAL, DOMAIN, + SERVICE_SELECT_FIRST, + SERVICE_SELECT_LAST, SERVICE_SELECT_NEXT, SERVICE_SELECT_OPTION, SERVICE_SELECT_PREVIOUS, @@ -25,8 +30,6 @@ from homeassistant.helpers import entity_registry from homeassistant.loader import bind_hass from homeassistant.setup import async_setup_component -# pylint: disable=protected-access -from tests.async_mock import patch from tests.common import mock_restore_cache @@ -103,6 +106,32 @@ def select_previous(hass, entity_id): ) +@bind_hass +def select_first(hass, entity_id): + """Set first value of input_select. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.async_create_task( + hass.services.async_call( + DOMAIN, SERVICE_SELECT_FIRST, {ATTR_ENTITY_ID: entity_id} + ) + ) + + +@bind_hass +def select_last(hass, entity_id): + """Set last value of input_select. + + This is a legacy helper method. Do not use it for new tests. + """ + hass.async_create_task( + hass.services.async_call( + DOMAIN, SERVICE_SELECT_LAST, {ATTR_ENTITY_ID: entity_id} + ) + ) + + async def test_config(hass): """Test config.""" invalid_configs = [ @@ -206,6 +235,38 @@ async def test_select_previous(hass): assert "last option" == state.state +async def test_select_first_last(hass): + """Test select_first and _last methods.""" + assert await async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + "test_1": { + "options": ["first option", "middle option", "last option"], + "initial": "middle option", + } + } + }, + ) + entity_id = "input_select.test_1" + + state = hass.states.get(entity_id) + assert "middle option" == state.state + + select_first(hass, entity_id) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert "first option" == state.state + + select_last(hass, entity_id) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert "last option" == state.state + + async def test_config_options(hass): """Test configuration options.""" count_start = len(hass.states.async_entity_ids()) diff --git a/tests/components/input_text/test_init.py b/tests/components/input_text/test_init.py index ed89ccd7087..cc226dc1d87 100644 --- a/tests/components/input_text/test_init.py +++ b/tests/components/input_text/test_init.py @@ -1,4 +1,7 @@ """The tests for the Input text component.""" +# pylint: disable=protected-access +from unittest.mock import patch + import pytest from homeassistant.components.input_text import ( @@ -26,8 +29,6 @@ from homeassistant.helpers import entity_registry from homeassistant.loader import bind_hass from homeassistant.setup import async_setup_component -# pylint: disable=protected-access -from tests.async_mock import patch from tests.common import mock_restore_cache TEST_VAL_MIN = 2 diff --git a/tests/components/insteon/mock_devices.py b/tests/components/insteon/mock_devices.py index f2afd6083a3..7ffb0672161 100644 --- a/tests/components/insteon/mock_devices.py +++ b/tests/components/insteon/mock_devices.py @@ -1,4 +1,6 @@ """Mock devices object to test Insteon.""" +from unittest.mock import AsyncMock, MagicMock + from pyinsteon.address import Address from pyinsteon.device_types import ( GeneralController_MiniRemote_4, @@ -6,8 +8,6 @@ from pyinsteon.device_types import ( SwitchedLightingControl_SwitchLinc, ) -from tests.async_mock import AsyncMock, MagicMock - class MockSwitchLinc(SwitchedLightingControl_SwitchLinc): """Mock SwitchLinc device.""" diff --git a/tests/components/insteon/test_config_flow.py b/tests/components/insteon/test_config_flow.py index 4e060b0d840..f1940b1eb39 100644 --- a/tests/components/insteon/test_config_flow.py +++ b/tests/components/insteon/test_config_flow.py @@ -1,5 +1,7 @@ """Test the config flow for the Insteon integration.""" +from unittest.mock import patch + from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.insteon.config_flow import ( HUB1, @@ -50,7 +52,6 @@ from .const import ( PATCH_CONNECTION, ) -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/insteon/test_init.py b/tests/components/insteon/test_init.py index 73620df6776..01546453868 100644 --- a/tests/components/insteon/test_init.py +++ b/tests/components/insteon/test_init.py @@ -1,5 +1,6 @@ """Test the init file for the Insteon component.""" import asyncio +from unittest.mock import patch from pyinsteon.address import Address @@ -40,7 +41,6 @@ from .const import ( ) from .mock_devices import MockDevices -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/integration/test_sensor.py b/tests/components/integration/test_sensor.py index c36a718ffb4..3afa5c14c22 100644 --- a/tests/components/integration/test_sensor.py +++ b/tests/components/integration/test_sensor.py @@ -1,12 +1,11 @@ """The tests for the integration sensor platform.""" from datetime import timedelta +from unittest.mock import patch from homeassistant.const import ENERGY_KILO_WATT_HOUR, POWER_WATT, TIME_SECONDS from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch - async def test_state(hass): """Test integration sensor state.""" diff --git a/tests/components/ipma/test_config_flow.py b/tests/components/ipma/test_config_flow.py index 98c1667b2f6..493bcfe28cf 100644 --- a/tests/components/ipma/test_config_flow.py +++ b/tests/components/ipma/test_config_flow.py @@ -1,5 +1,7 @@ """Tests for IPMA config flow.""" +from unittest.mock import Mock, patch + from homeassistant.components.ipma import DOMAIN, config_flow from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE from homeassistant.helpers import entity_registry @@ -7,7 +9,6 @@ from homeassistant.setup import async_setup_component from .test_weather import MockLocation -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry, mock_registry diff --git a/tests/components/ipma/test_weather.py b/tests/components/ipma/test_weather.py index 02e46816bbc..7ed1c4d3723 100644 --- a/tests/components/ipma/test_weather.py +++ b/tests/components/ipma/test_weather.py @@ -1,5 +1,6 @@ """The tests for the IPMA weather component.""" from collections import namedtuple +from unittest.mock import patch from homeassistant.components import weather from homeassistant.components.weather import ( @@ -21,7 +22,6 @@ from homeassistant.components.weather import ( from homeassistant.setup import async_setup_component from homeassistant.util.dt import now -from tests.async_mock import patch from tests.common import MockConfigEntry TEST_CONFIG = { diff --git a/tests/components/ipp/test_config_flow.py b/tests/components/ipp/test_config_flow.py index 24ead324243..140570c3c54 100644 --- a/tests/components/ipp/test_config_flow.py +++ b/tests/components/ipp/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for the IPP config flow.""" +from unittest.mock import patch + from homeassistant.components.ipp.const import CONF_BASE_PATH, CONF_UUID, DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SSL @@ -17,7 +19,6 @@ from . import ( mock_connection, ) -from tests.async_mock import patch from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/ipp/test_sensor.py b/tests/components/ipp/test_sensor.py index 1817a66f630..69143faec64 100644 --- a/tests/components/ipp/test_sensor.py +++ b/tests/components/ipp/test_sensor.py @@ -1,5 +1,6 @@ """Tests for the IPP sensor platform.""" from datetime import datetime +from unittest.mock import patch from homeassistant.components.ipp.const import DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -7,7 +8,6 @@ from homeassistant.const import ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util -from tests.async_mock import patch from tests.components.ipp import init_integration, mock_connection from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/iqvia/test_config_flow.py b/tests/components/iqvia/test_config_flow.py index 209f88cf895..5cc71302bc5 100644 --- a/tests/components/iqvia/test_config_flow.py +++ b/tests/components/iqvia/test_config_flow.py @@ -1,9 +1,10 @@ """Define tests for the IQVIA config flow.""" +from unittest.mock import patch + from homeassistant import data_entry_flow from homeassistant.components.iqvia import CONF_ZIP_CODE, DOMAIN from homeassistant.config_entries import SOURCE_USER -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/islamic_prayer_times/test_config_flow.py b/tests/components/islamic_prayer_times/test_config_flow.py index 3b966c9e861..b7a942e4f14 100644 --- a/tests/components/islamic_prayer_times/test_config_flow.py +++ b/tests/components/islamic_prayer_times/test_config_flow.py @@ -1,11 +1,12 @@ """Tests for Islamic Prayer Times config flow.""" +from unittest.mock import patch + import pytest from homeassistant import data_entry_flow from homeassistant.components import islamic_prayer_times from homeassistant.components.islamic_prayer_times.const import CONF_CALC_METHOD, DOMAIN -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/islamic_prayer_times/test_init.py b/tests/components/islamic_prayer_times/test_init.py index fd0de854056..850edc4b76d 100644 --- a/tests/components/islamic_prayer_times/test_init.py +++ b/tests/components/islamic_prayer_times/test_init.py @@ -1,6 +1,7 @@ """Tests for Islamic Prayer Times init.""" from datetime import timedelta +from unittest.mock import patch from prayer_times_calculator.exceptions import InvalidResponseError @@ -16,7 +17,6 @@ from . import ( PRAYER_TIMES_TIMESTAMPS, ) -from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/islamic_prayer_times/test_sensor.py b/tests/components/islamic_prayer_times/test_sensor.py index 13b69207cde..da89436a7e0 100644 --- a/tests/components/islamic_prayer_times/test_sensor.py +++ b/tests/components/islamic_prayer_times/test_sensor.py @@ -1,10 +1,11 @@ """The tests for the Islamic prayer times sensor platform.""" +from unittest.mock import patch + from homeassistant.components import islamic_prayer_times import homeassistant.util.dt as dt_util from . import NOW, PRAYER_TIMES, PRAYER_TIMES_TIMESTAMPS -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/isy994/test_config_flow.py b/tests/components/isy994/test_config_flow.py index 2fe19a0a9fd..c2236006b39 100644 --- a/tests/components/isy994/test_config_flow.py +++ b/tests/components/isy994/test_config_flow.py @@ -1,5 +1,7 @@ """Test the Universal Devices ISY994 config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components import ssdp from homeassistant.components.isy994.config_flow import CannotConnect @@ -17,7 +19,6 @@ from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import patch from tests.common import MockConfigEntry MOCK_HOSTNAME = "1.1.1.1" diff --git a/tests/components/izone/test_config_flow.py b/tests/components/izone/test_config_flow.py index df5ec18db8a..a548e59930b 100644 --- a/tests/components/izone/test_config_flow.py +++ b/tests/components/izone/test_config_flow.py @@ -1,12 +1,12 @@ """Tests for iZone.""" +from unittest.mock import Mock, patch + import pytest from homeassistant import config_entries, data_entry_flow from homeassistant.components.izone.const import DISPATCH_CONTROLLER_DISCOVERED, IZONE -from tests.async_mock import Mock, patch - @pytest.fixture def mock_disco(): diff --git a/tests/components/jewish_calendar/__init__.py b/tests/components/jewish_calendar/__init__.py index c5fc2683923..2d42458cf1b 100644 --- a/tests/components/jewish_calendar/__init__.py +++ b/tests/components/jewish_calendar/__init__.py @@ -2,12 +2,11 @@ from collections import namedtuple from contextlib import contextmanager from datetime import datetime +from unittest.mock import patch from homeassistant.components import jewish_calendar import homeassistant.util.dt as dt_util -from tests.async_mock import patch - _LatLng = namedtuple("_LatLng", ["lat", "lng"]) HDATE_DEFAULT_ALTITUDE = 754 diff --git a/tests/components/juicenet/test_config_flow.py b/tests/components/juicenet/test_config_flow.py index 3a7d2f06c43..817ae04aa79 100644 --- a/tests/components/juicenet/test_config_flow.py +++ b/tests/components/juicenet/test_config_flow.py @@ -1,4 +1,6 @@ """Test the JuiceNet config flow.""" +from unittest.mock import MagicMock, patch + import aiohttp from pyjuicenet import TokenError @@ -6,8 +8,6 @@ from homeassistant import config_entries, setup from homeassistant.components.juicenet.const import DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN -from tests.async_mock import MagicMock, patch - def _mock_juicenet_return_value(get_devices=None): juicenet_mock = MagicMock() diff --git a/tests/components/kira/test_init.py b/tests/components/kira/test_init.py index db8eb6b2456..93f3ee4d82a 100644 --- a/tests/components/kira/test_init.py +++ b/tests/components/kira/test_init.py @@ -3,14 +3,13 @@ import os import shutil import tempfile +from unittest.mock import MagicMock, patch import pytest import homeassistant.components.kira as kira from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, patch - TEST_CONFIG = { kira.DOMAIN: { "sensors": [ diff --git a/tests/components/kira/test_remote.py b/tests/components/kira/test_remote.py index c946823474c..e91cbaca891 100644 --- a/tests/components/kira/test_remote.py +++ b/tests/components/kira/test_remote.py @@ -1,9 +1,9 @@ """The tests for Kira sensor platform.""" import unittest +from unittest.mock import MagicMock from homeassistant.components.kira import remote as kira -from tests.async_mock import MagicMock from tests.common import get_test_home_assistant SERVICE_SEND_COMMAND = "send_command" diff --git a/tests/components/kira/test_sensor.py b/tests/components/kira/test_sensor.py index 21572a8735b..cd4bee60ae6 100644 --- a/tests/components/kira/test_sensor.py +++ b/tests/components/kira/test_sensor.py @@ -1,9 +1,9 @@ """The tests for Kira sensor platform.""" import unittest +from unittest.mock import MagicMock from homeassistant.components.kira import sensor as kira -from tests.async_mock import MagicMock from tests.common import get_test_home_assistant TEST_CONFIG = {kira.DOMAIN: {"sensors": [{"host": "127.0.0.1", "port": 17324}]}} diff --git a/tests/components/kodi/__init__.py b/tests/components/kodi/__init__.py index 31c9dff14ac..bef576dd6bf 100644 --- a/tests/components/kodi/__init__.py +++ b/tests/components/kodi/__init__.py @@ -1,4 +1,6 @@ """Tests for the Kodi integration.""" +from unittest.mock import patch + from homeassistant.components.kodi.const import CONF_WS_PORT, DOMAIN from homeassistant.const import ( CONF_HOST, @@ -11,7 +13,6 @@ from homeassistant.const import ( from .util import MockConnection -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/kodi/test_config_flow.py b/tests/components/kodi/test_config_flow.py index 9e892033786..cba567e5bb5 100644 --- a/tests/components/kodi/test_config_flow.py +++ b/tests/components/kodi/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Kodi config flow.""" +from unittest.mock import AsyncMock, PropertyMock, patch + import pytest from homeassistant import config_entries @@ -21,7 +23,6 @@ from .util import ( get_kodi_connection, ) -from tests.async_mock import AsyncMock, PropertyMock, patch from tests.common import MockConfigEntry diff --git a/tests/components/kodi/test_init.py b/tests/components/kodi/test_init.py index b272e005012..aa206270d35 100644 --- a/tests/components/kodi/test_init.py +++ b/tests/components/kodi/test_init.py @@ -1,11 +1,11 @@ """Test the Kodi integration init.""" +from unittest.mock import patch + from homeassistant.components.kodi.const import DOMAIN from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED from . import init_integration -from tests.async_mock import patch - async def test_unload_entry(hass): """Test successful unload of entry.""" diff --git a/tests/components/konnected/test_config_flow.py b/tests/components/konnected/test_config_flow.py index dfd4600f1fb..4b5cc602f99 100644 --- a/tests/components/konnected/test_config_flow.py +++ b/tests/components/konnected/test_config_flow.py @@ -1,10 +1,11 @@ """Tests for Konnected Alarm Panel config flow.""" +from unittest.mock import patch + import pytest from homeassistant.components import konnected from homeassistant.components.konnected import config_flow -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/konnected/test_init.py b/tests/components/konnected/test_init.py index c198812a82b..91d6633cf1d 100644 --- a/tests/components/konnected/test_init.py +++ b/tests/components/konnected/test_init.py @@ -1,4 +1,6 @@ """Test Konnected setup process.""" +from unittest.mock import patch + import pytest from homeassistant.components import konnected @@ -7,7 +9,6 @@ from homeassistant.config import async_process_ha_core_config from homeassistant.const import HTTP_NOT_FOUND from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/konnected/test_panel.py b/tests/components/konnected/test_panel.py index 21351cb6be3..38507aa973c 100644 --- a/tests/components/konnected/test_panel.py +++ b/tests/components/konnected/test_panel.py @@ -1,5 +1,6 @@ """Test Konnected setup process.""" from datetime import timedelta +from unittest.mock import patch import pytest @@ -7,7 +8,6 @@ from homeassistant.components.konnected import config_flow, panel from homeassistant.setup import async_setup_component from homeassistant.util import utcnow -from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/kulersky/test_config_flow.py b/tests/components/kulersky/test_config_flow.py index 59e3188fd7e..24f3f9a010e 100644 --- a/tests/components/kulersky/test_config_flow.py +++ b/tests/components/kulersky/test_config_flow.py @@ -1,11 +1,11 @@ """Test the Kuler Sky config flow.""" +from unittest.mock import patch + import pykulersky from homeassistant import config_entries, setup from homeassistant.components.kulersky.config_flow import DOMAIN -from tests.async_mock import patch - async def test_flow_success(hass): """Test we get the form.""" diff --git a/tests/components/kulersky/test_light.py b/tests/components/kulersky/test_light.py index 5403f7cedde..fd5db92908b 100644 --- a/tests/components/kulersky/test_light.py +++ b/tests/components/kulersky/test_light.py @@ -1,5 +1,6 @@ """Test the Kuler Sky lights.""" import asyncio +from unittest.mock import MagicMock, patch import pykulersky import pytest @@ -27,7 +28,6 @@ from homeassistant.const import ( ) import homeassistant.util.dt as dt_util -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/lastfm/test_sensor.py b/tests/components/lastfm/test_sensor.py index 82c789415bf..af7a177edb8 100644 --- a/tests/components/lastfm/test_sensor.py +++ b/tests/components/lastfm/test_sensor.py @@ -1,4 +1,6 @@ """Tests for the lastfm sensor.""" +from unittest.mock import patch + from pylast import Track import pytest @@ -6,8 +8,6 @@ from homeassistant.components import sensor from homeassistant.components.lastfm.sensor import STATE_NOT_SCROBBLING from homeassistant.setup import async_setup_component -from tests.async_mock import patch - class MockUser: """Mock user object for pylast.""" diff --git a/tests/components/light/conftest.py b/tests/components/light/conftest.py index 67af99a3d6c..12bd62edcb7 100644 --- a/tests/components/light/conftest.py +++ b/tests/components/light/conftest.py @@ -1,11 +1,11 @@ """Light conftest.""" +from unittest.mock import AsyncMock, patch + import pytest from homeassistant.components.light import Profiles -from tests.async_mock import AsyncMock, patch - @pytest.fixture(autouse=True) def mock_light_profiles(): @@ -20,7 +20,6 @@ def mock_light_profiles(): with patch( "homeassistant.components.light.Profiles", - SCHEMA=Profiles.SCHEMA, side_effect=mock_profiles_class, ): yield data diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py index eea443b7d34..2a877478b1e 100644 --- a/tests/components/light/test_device_condition.py +++ b/tests/components/light/test_device_condition.py @@ -1,5 +1,6 @@ """The test for light device automation.""" from datetime import timedelta +from unittest.mock import patch import pytest @@ -10,7 +11,6 @@ from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import ( MockConfigEntry, async_get_device_automation_capabilities, diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 72674a984fd..c36fa623e37 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -1,4 +1,6 @@ """The tests for the Light component.""" +from unittest.mock import MagicMock, mock_open, patch + import pytest import voluptuous as vol @@ -16,7 +18,6 @@ from homeassistant.const import ( ) from homeassistant.exceptions import Unauthorized from homeassistant.setup import async_setup_component -from homeassistant.util import color from tests.common import async_mock_service @@ -280,14 +281,14 @@ async def test_services(hass, mock_light_profiles): assert data == {} # One of the light profiles - mock_light_profiles["relax"] = (35.932, 69.412, 144, 0) - prof_name, prof_h, prof_s, prof_bri, prof_t = "relax", 35.932, 69.412, 144, 0 + profile = light.Profile("relax", 0.513, 0.413, 144, 0) + mock_light_profiles[profile.name] = profile # Test light profiles await hass.services.async_call( light.DOMAIN, SERVICE_TURN_ON, - {ATTR_ENTITY_ID: ent1.entity_id, light.ATTR_PROFILE: prof_name}, + {ATTR_ENTITY_ID: ent1.entity_id, light.ATTR_PROFILE: profile.name}, blocking=True, ) # Specify a profile and a brightness attribute to overwrite it @@ -296,7 +297,7 @@ async def test_services(hass, mock_light_profiles): SERVICE_TURN_ON, { ATTR_ENTITY_ID: ent2.entity_id, - light.ATTR_PROFILE: prof_name, + light.ATTR_PROFILE: profile.name, light.ATTR_BRIGHTNESS: 100, light.ATTR_TRANSITION: 1, }, @@ -305,15 +306,15 @@ async def test_services(hass, mock_light_profiles): _, data = ent1.last_call("turn_on") assert data == { - light.ATTR_BRIGHTNESS: prof_bri, - light.ATTR_HS_COLOR: (prof_h, prof_s), - light.ATTR_TRANSITION: prof_t, + light.ATTR_BRIGHTNESS: profile.brightness, + light.ATTR_HS_COLOR: profile.hs_color, + light.ATTR_TRANSITION: profile.transition, } _, data = ent2.last_call("turn_on") assert data == { light.ATTR_BRIGHTNESS: 100, - light.ATTR_HS_COLOR: (prof_h, prof_s), + light.ATTR_HS_COLOR: profile.hs_color, light.ATTR_TRANSITION: 1, } @@ -323,7 +324,7 @@ async def test_services(hass, mock_light_profiles): SERVICE_TOGGLE, { ATTR_ENTITY_ID: ent3.entity_id, - light.ATTR_PROFILE: prof_name, + light.ATTR_PROFILE: profile.name, light.ATTR_BRIGHTNESS_PCT: 100, }, blocking=True, @@ -332,8 +333,8 @@ async def test_services(hass, mock_light_profiles): _, data = ent3.last_call("turn_on") assert data == { light.ATTR_BRIGHTNESS: 255, - light.ATTR_HS_COLOR: (prof_h, prof_s), - light.ATTR_TRANSITION: prof_t, + light.ATTR_HS_COLOR: profile.hs_color, + light.ATTR_TRANSITION: profile.transition, } await hass.services.async_call( @@ -392,7 +393,7 @@ async def test_services(hass, mock_light_profiles): SERVICE_TURN_ON, { ATTR_ENTITY_ID: ent1.entity_id, - light.ATTR_PROFILE: prof_name, + light.ATTR_PROFILE: profile.name, light.ATTR_BRIGHTNESS: "bright", }, blocking=True, @@ -422,13 +423,92 @@ async def test_services(hass, mock_light_profiles): assert data == {} -async def test_light_profiles(hass, mock_light_profiles): +@pytest.mark.parametrize( + "profile_name, last_call, expected_data", + ( + ( + "test", + "turn_on", + { + light.ATTR_HS_COLOR: (71.059, 100), + light.ATTR_BRIGHTNESS: 100, + light.ATTR_TRANSITION: 0, + }, + ), + ( + "color_no_brightness_no_transition", + "turn_on", + { + light.ATTR_HS_COLOR: (71.059, 100), + }, + ), + ( + "no color", + "turn_on", + { + light.ATTR_BRIGHTNESS: 110, + light.ATTR_TRANSITION: 0, + }, + ), + ( + "test_off", + "turn_off", + { + light.ATTR_TRANSITION: 0, + }, + ), + ( + "no brightness", + "turn_on", + { + light.ATTR_HS_COLOR: (71.059, 100), + }, + ), + ( + "color_and_brightness", + "turn_on", + { + light.ATTR_HS_COLOR: (71.059, 100), + light.ATTR_BRIGHTNESS: 120, + }, + ), + ( + "color_and_transition", + "turn_on", + { + light.ATTR_HS_COLOR: (71.059, 100), + light.ATTR_TRANSITION: 4.2, + }, + ), + ( + "brightness_and_transition", + "turn_on", + { + light.ATTR_BRIGHTNESS: 130, + light.ATTR_TRANSITION: 5.3, + }, + ), + ), +) +async def test_light_profiles( + hass, mock_light_profiles, profile_name, expected_data, last_call +): """Test light profiles.""" platform = getattr(hass.components, "test.light") platform.init() - mock_light_profiles["test"] = color.color_xy_to_hs(0.4, 0.6) + (100, 0) - mock_light_profiles["test_off"] = 0, 0, 0, 0 + profile_mock_data = { + "test": (0.4, 0.6, 100, 0), + "color_no_brightness_no_transition": (0.4, 0.6, None, None), + "no color": (None, None, 110, 0), + "test_off": (0, 0, 0, 0), + "no brightness": (0.4, 0.6, None), + "color_and_brightness": (0.4, 0.6, 120), + "color_and_transition": (0.4, 0.6, None, 4.2), + "brightness_and_transition": (None, None, 130, 5.3), + } + for name, data in profile_mock_data.items(): + mock_light_profiles[name] = light.Profile(*(name, *data)) assert await async_setup_component( hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} @@ -442,29 +522,17 @@ async def test_light_profiles(hass, mock_light_profiles): SERVICE_TURN_ON, { ATTR_ENTITY_ID: ent1.entity_id, - light.ATTR_PROFILE: "test", + light.ATTR_PROFILE: profile_name, }, blocking=True, ) - _, data = ent1.last_call("turn_on") - assert light.is_on(hass, ent1.entity_id) - assert data == { - light.ATTR_HS_COLOR: (71.059, 100), - light.ATTR_BRIGHTNESS: 100, - light.ATTR_TRANSITION: 0, - } - - await hass.services.async_call( - light.DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: ent1.entity_id, light.ATTR_PROFILE: "test_off"}, - blocking=True, - ) - - _, data = ent1.last_call("turn_off") - assert not light.is_on(hass, ent1.entity_id) - assert data == {light.ATTR_TRANSITION: 0} + _, data = ent1.last_call(last_call) + if last_call == "turn_on": + assert light.is_on(hass, ent1.entity_id) + else: + assert not light.is_on(hass, ent1.entity_id) + assert data == expected_data async def test_default_profiles_group(hass, mock_light_profiles): @@ -477,10 +545,8 @@ async def test_default_profiles_group(hass, mock_light_profiles): ) await hass.async_block_till_done() - mock_light_profiles["group.all_lights.default"] = color.color_xy_to_hs(0.4, 0.6) + ( - 99, - 2, - ) + profile = light.Profile("group.all_lights.default", 0.4, 0.6, 99, 2) + mock_light_profiles[profile.name] = profile ent, _, _ = platform.ENTITIES await hass.services.async_call( @@ -495,7 +561,58 @@ async def test_default_profiles_group(hass, mock_light_profiles): } -async def test_default_profiles_light(hass, mock_light_profiles): +@pytest.mark.parametrize( + "extra_call_params, expected_params", + ( + ( + {}, + { + light.ATTR_HS_COLOR: (50.353, 100), + light.ATTR_BRIGHTNESS: 100, + light.ATTR_TRANSITION: 3, + }, + ), + ( + {light.ATTR_BRIGHTNESS: 22}, + { + light.ATTR_HS_COLOR: (50.353, 100), + light.ATTR_BRIGHTNESS: 22, + light.ATTR_TRANSITION: 3, + }, + ), + ( + {light.ATTR_TRANSITION: 22}, + { + light.ATTR_HS_COLOR: (50.353, 100), + light.ATTR_BRIGHTNESS: 100, + light.ATTR_TRANSITION: 22, + }, + ), + ( + { + light.ATTR_XY_COLOR: [0.4448, 0.4066], + light.ATTR_BRIGHTNESS: 11, + light.ATTR_TRANSITION: 1, + }, + { + light.ATTR_HS_COLOR: (38.88, 49.02), + light.ATTR_BRIGHTNESS: 11, + light.ATTR_TRANSITION: 1, + }, + ), + ( + {light.ATTR_BRIGHTNESS: 11, light.ATTR_TRANSITION: 1}, + { + light.ATTR_HS_COLOR: (50.353, 100), + light.ATTR_BRIGHTNESS: 11, + light.ATTR_TRANSITION: 1, + }, + ), + ), +) +async def test_default_profiles_light( + hass, mock_light_profiles, extra_call_params, expected_params +): """Test default turn-on light profile for a specific light.""" platform = getattr(hass.components, "test.light") platform.init() @@ -505,14 +622,10 @@ async def test_default_profiles_light(hass, mock_light_profiles): ) await hass.async_block_till_done() - mock_light_profiles["group.all_lights.default"] = color.color_xy_to_hs(0.3, 0.5) + ( - 200, - 0, - ) - mock_light_profiles["light.ceiling_2.default"] = color.color_xy_to_hs(0.6, 0.6) + ( - 100, - 3, - ) + profile = light.Profile("group.all_lights.default", 0.3, 0.5, 200, 0) + mock_light_profiles[profile.name] = profile + profile = light.Profile("light.ceiling_2.default", 0.6, 0.6, 100, 3) + mock_light_profiles[profile.name] = profile dev = next(filter(lambda x: x.entity_id == "light.ceiling_2", platform.ENTITIES)) await hass.services.async_call( @@ -520,14 +633,26 @@ async def test_default_profiles_light(hass, mock_light_profiles): SERVICE_TURN_ON, { ATTR_ENTITY_ID: dev.entity_id, + **extra_call_params, }, blocking=True, ) _, data = dev.last_call("turn_on") + assert data == expected_params + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: dev.entity_id, + light.ATTR_BRIGHTNESS: 0, + }, + blocking=True, + ) + + _, data = dev.last_call("turn_off") assert data == { - light.ATTR_HS_COLOR: (50.353, 100), - light.ATTR_BRIGHTNESS: 100, light.ATTR_TRANSITION: 3, } @@ -693,8 +818,85 @@ async def test_profiles(hass): profiles = orig_Profiles(hass) await profiles.async_initialize() assert profiles.data == { - "concentrate": (35.932, 69.412, 219, 0), - "energize": (43.333, 21.176, 203, 0), - "reading": (38.88, 49.02, 240, 0), - "relax": (35.932, 69.412, 144, 0), + "concentrate": light.Profile("concentrate", 0.5119, 0.4147, 219, None), + "energize": light.Profile("energize", 0.368, 0.3686, 203, None), + "reading": light.Profile("reading", 0.4448, 0.4066, 240, None), + "relax": light.Profile("relax", 0.5119, 0.4147, 144, None), } + assert profiles.data["concentrate"].hs_color == (35.932, 69.412) + assert profiles.data["energize"].hs_color == (43.333, 21.176) + assert profiles.data["reading"].hs_color == (38.88, 49.02) + assert profiles.data["relax"].hs_color == (35.932, 69.412) + + +@patch("os.path.isfile", MagicMock(side_effect=(True, False))) +async def test_profile_load_optional_hs_color(hass): + """Test profile loading with profiles containing no xy color.""" + + csv_file = """the first line is skipped +no_color,,,100,1 +no_color_no_transition,,,110 +color,0.5119,0.4147,120,2 +color_no_transition,0.4448,0.4066,130 +color_and_brightness,0.4448,0.4066,170, +only_brightness,,,140 +only_transition,,,,150 +transition_float,,,,1.6 +invalid_profile_1, +invalid_color_2,,0.1,1,2 +invalid_color_3,,0.1,1 +invalid_color_4,0.1,,1,3 +invalid_color_5,0.1,,1 +invalid_brightness,0,0,256,4 +invalid_brightness_2,0,0,256 +invalid_no_brightness_no_color_no_transition,,, +""" + + profiles = orig_Profiles(hass) + with patch("builtins.open", mock_open(read_data=csv_file)): + await profiles.async_initialize() + await hass.async_block_till_done() + + assert profiles.data["no_color"].hs_color is None + assert profiles.data["no_color"].brightness == 100 + assert profiles.data["no_color"].transition == 1 + + assert profiles.data["no_color_no_transition"].hs_color is None + assert profiles.data["no_color_no_transition"].brightness == 110 + assert profiles.data["no_color_no_transition"].transition is None + + assert profiles.data["color"].hs_color == (35.932, 69.412) + assert profiles.data["color"].brightness == 120 + assert profiles.data["color"].transition == 2 + + assert profiles.data["color_no_transition"].hs_color == (38.88, 49.02) + assert profiles.data["color_no_transition"].brightness == 130 + assert profiles.data["color_no_transition"].transition is None + + assert profiles.data["color_and_brightness"].hs_color == (38.88, 49.02) + assert profiles.data["color_and_brightness"].brightness == 170 + assert profiles.data["color_and_brightness"].transition is None + + assert profiles.data["only_brightness"].hs_color is None + assert profiles.data["only_brightness"].brightness == 140 + assert profiles.data["only_brightness"].transition is None + + assert profiles.data["only_transition"].hs_color is None + assert profiles.data["only_transition"].brightness is None + assert profiles.data["only_transition"].transition == 150 + + assert profiles.data["transition_float"].hs_color is None + assert profiles.data["transition_float"].brightness is None + assert profiles.data["transition_float"].transition == 1.6 + + for invalid_profile_name in ( + "invalid_profile_1", + "invalid_color_2", + "invalid_color_3", + "invalid_color_4", + "invalid_color_5", + "invalid_brightness", + "invalid_brightness_2", + "invalid_no_brightness_no_color_no_transition", + ): + assert invalid_profile_name not in profiles.data diff --git a/tests/components/light/test_significant_change.py b/tests/components/light/test_significant_change.py new file mode 100644 index 00000000000..f935ec8df1a --- /dev/null +++ b/tests/components/light/test_significant_change.py @@ -0,0 +1,70 @@ +"""Test the Light significant change platform.""" +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_HS_COLOR, + ATTR_WHITE_VALUE, +) +from homeassistant.components.light.significant_change import ( + async_check_significant_change, +) + + +async def test_significant_change(): + """Detect Light significant changes.""" + assert not async_check_significant_change(None, "on", {}, "on", {}) + assert async_check_significant_change(None, "on", {}, "off", {}) + + # Brightness + assert not async_check_significant_change( + None, "on", {ATTR_BRIGHTNESS: 60}, "on", {ATTR_BRIGHTNESS: 61} + ) + assert async_check_significant_change( + None, "on", {ATTR_BRIGHTNESS: 60}, "on", {ATTR_BRIGHTNESS: 63} + ) + + # Color temp + assert not async_check_significant_change( + None, "on", {ATTR_COLOR_TEMP: 60}, "on", {ATTR_COLOR_TEMP: 64} + ) + assert async_check_significant_change( + None, "on", {ATTR_COLOR_TEMP: 60}, "on", {ATTR_COLOR_TEMP: 65} + ) + + # White value + assert not async_check_significant_change( + None, "on", {ATTR_WHITE_VALUE: 60}, "on", {ATTR_WHITE_VALUE: 64} + ) + assert async_check_significant_change( + None, "on", {ATTR_WHITE_VALUE: 60}, "on", {ATTR_WHITE_VALUE: 65} + ) + + # Effect + for eff1, eff2, expected in ( + (None, None, False), + (None, "colorloop", True), + ("colorloop", None, True), + ("colorloop", "jump", True), + ("colorloop", "colorloop", False), + ): + result = async_check_significant_change( + None, "on", {ATTR_EFFECT: eff1}, "on", {ATTR_EFFECT: eff2} + ) + assert result is expected + + # Hue + assert not async_check_significant_change( + None, "on", {ATTR_HS_COLOR: [120, 20]}, "on", {ATTR_HS_COLOR: [124, 20]} + ) + assert async_check_significant_change( + None, "on", {ATTR_HS_COLOR: [120, 20]}, "on", {ATTR_HS_COLOR: [125, 20]} + ) + + # Satursation + assert not async_check_significant_change( + None, "on", {ATTR_HS_COLOR: [120, 20]}, "on", {ATTR_HS_COLOR: [120, 22]} + ) + assert async_check_significant_change( + None, "on", {ATTR_HS_COLOR: [120, 20]}, "on", {ATTR_HS_COLOR: [120, 23]} + ) diff --git a/tests/components/locative/test_init.py b/tests/components/locative/test_init.py index 2fa68778f7e..d183cb9814a 100644 --- a/tests/components/locative/test_init.py +++ b/tests/components/locative/test_init.py @@ -1,4 +1,6 @@ """The tests the for Locative device tracker platform.""" +from unittest.mock import patch + import pytest from homeassistant import data_entry_flow @@ -10,8 +12,6 @@ from homeassistant.const import HTTP_OK, HTTP_UNPROCESSABLE_ENTITY from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component -from tests.async_mock import patch - # pylint: disable=redefined-outer-name diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 36d47115acc..cd3fb519ded 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -3,6 +3,7 @@ import collections from datetime import datetime, timedelta import json +from unittest.mock import Mock, patch import pytest import voluptuous as vol @@ -36,7 +37,6 @@ from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component, setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import Mock, patch from tests.common import get_test_home_assistant, init_recorder_component, mock_platform from tests.components.recorder.common import trigger_db_commit @@ -282,6 +282,7 @@ def create_state_changed_event_from_old_new( "time_fired" "context_id" "context_user_id" + "context_parent_id" "state" "entity_id" "domain" @@ -300,6 +301,7 @@ def create_state_changed_event_from_old_new( row.domain = entity_id and ha.split_entity_id(entity_id)[0] row.context_id = None row.context_user_id = None + row.context_parent_id = None row.old_state_id = old_state and 1 row.state_id = new_state and 1 return logbook.LazyEventPartialState(row) @@ -946,6 +948,187 @@ async def test_logbook_entity_context_id(hass, hass_client): assert json_dict[7]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474" +async def test_logbook_entity_context_parent_id(hass, hass_client): + """Test the logbook view links events via context parent_id.""" + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "logbook", {}) + await async_setup_component(hass, "automation", {}) + await async_setup_component(hass, "script", {}) + + await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + context = ha.Context( + id="ac5bd62de45711eaaeb351041eec8dd9", + user_id="b400facee45711eaa9308bfd3d19e474", + ) + + # An Automation triggering scripts with a new context + automation_entity_id_test = "automation.alarm" + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation", ATTR_ENTITY_ID: automation_entity_id_test}, + context=context, + ) + + child_context = ha.Context( + id="2798bfedf8234b5e9f4009c91f48f30c", + parent_id="ac5bd62de45711eaaeb351041eec8dd9", + user_id="b400facee45711eaa9308bfd3d19e474", + ) + hass.bus.async_fire( + EVENT_SCRIPT_STARTED, + {ATTR_NAME: "Mock script", ATTR_ENTITY_ID: "script.mock_script"}, + context=child_context, + ) + hass.states.async_set( + automation_entity_id_test, + STATE_ON, + {ATTR_FRIENDLY_NAME: "Alarm Automation"}, + context=child_context, + ) + + entity_id_test = "alarm_control_panel.area_001" + hass.states.async_set(entity_id_test, STATE_OFF, context=child_context) + await hass.async_block_till_done() + hass.states.async_set(entity_id_test, STATE_ON, context=child_context) + await hass.async_block_till_done() + entity_id_second = "alarm_control_panel.area_002" + hass.states.async_set(entity_id_second, STATE_OFF, context=child_context) + await hass.async_block_till_done() + hass.states.async_set(entity_id_second, STATE_ON, context=child_context) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + logbook.async_log_entry( + hass, + "mock_name", + "mock_message", + "alarm_control_panel", + "alarm_control_panel.area_003", + child_context, + ) + await hass.async_block_till_done() + + logbook.async_log_entry( + hass, + "mock_name", + "mock_message", + "homeassistant", + None, + child_context, + ) + await hass.async_block_till_done() + + # A state change via service call with the script as the parent + light_turn_off_service_context = ha.Context( + id="9c5bd62de45711eaaeb351041eec8dd9", + parent_id="2798bfedf8234b5e9f4009c91f48f30c", + user_id="9400facee45711eaa9308bfd3d19e474", + ) + hass.states.async_set("light.switch", STATE_ON) + await hass.async_block_till_done() + + hass.bus.async_fire( + EVENT_CALL_SERVICE, + { + ATTR_DOMAIN: "light", + ATTR_SERVICE: "turn_off", + ATTR_ENTITY_ID: "light.switch", + }, + context=light_turn_off_service_context, + ) + await hass.async_block_till_done() + + hass.states.async_set( + "light.switch", STATE_OFF, context=light_turn_off_service_context + ) + await hass.async_block_till_done() + + # An event with a parent event, but the parent event isn't available + missing_parent_context = ha.Context( + id="fc40b9a0d1f246f98c34b33c76228ee6", + parent_id="c8ce515fe58e442f8664246c65ed964f", + user_id="485cacf93ef84d25a99ced3126b921d2", + ) + logbook.async_log_entry( + hass, + "mock_name", + "mock_message", + "alarm_control_panel", + "alarm_control_panel.area_009", + missing_parent_context, + ) + await hass.async_block_till_done() + + await hass.async_add_executor_job(trigger_db_commit, hass) + await hass.async_block_till_done() + await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + client = await hass_client() + + # Today time 00:00:00 + start = dt_util.utcnow().date() + start_date = datetime(start.year, start.month, start.day) + + # Test today entries with filter by end_time + end_time = start + timedelta(hours=24) + response = await client.get( + f"/api/logbook/{start_date.isoformat()}?end_time={end_time}" + ) + assert response.status == 200 + json_dict = await response.json() + + assert json_dict[0]["entity_id"] == "automation.alarm" + assert "context_entity_id" not in json_dict[0] + assert json_dict[0]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + + # New context, so this looks to be triggered by the Alarm Automation + assert json_dict[1]["entity_id"] == "script.mock_script" + assert json_dict[1]["context_event_type"] == "automation_triggered" + assert json_dict[1]["context_entity_id"] == "automation.alarm" + assert json_dict[1]["context_entity_id_name"] == "Alarm Automation" + assert json_dict[1]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + + assert json_dict[2]["entity_id"] == entity_id_test + assert json_dict[2]["context_event_type"] == "script_started" + assert json_dict[2]["context_entity_id"] == "script.mock_script" + assert json_dict[2]["context_entity_id_name"] == "mock script" + assert json_dict[2]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + + assert json_dict[3]["entity_id"] == entity_id_second + assert json_dict[3]["context_event_type"] == "script_started" + assert json_dict[3]["context_entity_id"] == "script.mock_script" + assert json_dict[3]["context_entity_id_name"] == "mock script" + assert json_dict[3]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + + assert json_dict[4]["domain"] == "homeassistant" + + assert json_dict[5]["entity_id"] == "alarm_control_panel.area_003" + assert json_dict[5]["context_event_type"] == "script_started" + assert json_dict[5]["context_entity_id"] == "script.mock_script" + assert json_dict[5]["domain"] == "alarm_control_panel" + assert json_dict[5]["context_entity_id_name"] == "mock script" + assert json_dict[5]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + + assert json_dict[6]["domain"] == "homeassistant" + assert json_dict[6]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + + assert json_dict[7]["entity_id"] == "light.switch" + assert json_dict[7]["context_event_type"] == "call_service" + assert json_dict[7]["context_domain"] == "light" + assert json_dict[7]["context_service"] == "turn_off" + assert json_dict[7]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474" + + assert json_dict[8]["entity_id"] == "alarm_control_panel.area_009" + assert json_dict[8]["domain"] == "alarm_control_panel" + assert "context_event_type" not in json_dict[8] + assert "context_entity_id" not in json_dict[8] + assert "context_entity_id_name" not in json_dict[8] + assert json_dict[8]["context_user_id"] == "485cacf93ef84d25a99ced3126b921d2" + + async def test_logbook_context_from_template(hass, hass_client): """Test the logbook view with end_time and entity with automations and scripts.""" await hass.async_add_executor_job(init_recorder_component, hass) diff --git a/tests/components/logentries/test_init.py b/tests/components/logentries/test_init.py index 22cbfb41c76..3ae25d521cb 100644 --- a/tests/components/logentries/test_init.py +++ b/tests/components/logentries/test_init.py @@ -1,13 +1,13 @@ """The tests for the Logentries component.""" +from unittest.mock import MagicMock, call, patch + import pytest import homeassistant.components.logentries as logentries from homeassistant.const import EVENT_STATE_CHANGED, STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, call, patch - async def test_setup_config_full(hass): """Test setup with all data.""" diff --git a/tests/components/logger/test_init.py b/tests/components/logger/test_init.py index 571df44bce5..abb8c9b0de8 100644 --- a/tests/components/logger/test_init.py +++ b/tests/components/logger/test_init.py @@ -1,6 +1,7 @@ """The tests for the Logger component.""" from collections import defaultdict import logging +from unittest.mock import Mock, patch import pytest @@ -8,8 +9,6 @@ from homeassistant.components import logger from homeassistant.components.logger import LOGSEVERITY from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch - HASS_NS = "unused.homeassistant" COMPONENTS_NS = f"{HASS_NS}.components" ZONE_NS = f"{COMPONENTS_NS}.zone" diff --git a/tests/components/logi_circle/test_config_flow.py b/tests/components/logi_circle/test_config_flow.py index c743cce7519..28335b93ad9 100644 --- a/tests/components/logi_circle/test_config_flow.py +++ b/tests/components/logi_circle/test_config_flow.py @@ -1,6 +1,6 @@ """Tests for Logi Circle config flow.""" import asyncio -from unittest.mock import Mock, patch +from unittest.mock import AsyncMock, Mock, patch import pytest @@ -13,7 +13,6 @@ from homeassistant.components.logi_circle.config_flow import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock from tests.common import mock_coro diff --git a/tests/components/lovelace/test_dashboard.py b/tests/components/lovelace/test_dashboard.py index b5157dd7c46..dd87e2bc275 100644 --- a/tests/components/lovelace/test_dashboard.py +++ b/tests/components/lovelace/test_dashboard.py @@ -1,11 +1,12 @@ """Test the Lovelace initialization.""" +from unittest.mock import patch + import pytest from homeassistant.components import frontend from homeassistant.components.lovelace import const, dashboard from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import assert_setup_component, async_capture_events diff --git a/tests/components/lovelace/test_resources.py b/tests/components/lovelace/test_resources.py index d32dc9388f1..86c0a052776 100644 --- a/tests/components/lovelace/test_resources.py +++ b/tests/components/lovelace/test_resources.py @@ -1,12 +1,11 @@ """Test Lovelace resources.""" import copy +from unittest.mock import patch import uuid from homeassistant.components.lovelace import dashboard, resources from homeassistant.setup import async_setup_component -from tests.async_mock import patch - RESOURCE_EXAMPLES = [ {"type": "js", "url": "/local/bla.js"}, {"type": "css", "url": "/local/bla.css"}, diff --git a/tests/components/lovelace/test_system_health.py b/tests/components/lovelace/test_system_health.py index b81915083d2..780a0678940 100644 --- a/tests/components/lovelace/test_system_health.py +++ b/tests/components/lovelace/test_system_health.py @@ -1,8 +1,9 @@ """Tests for Lovelace system health.""" +from unittest.mock import patch + from homeassistant.components.lovelace import dashboard from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import get_system_health_info diff --git a/tests/components/luftdaten/test_config_flow.py b/tests/components/luftdaten/test_config_flow.py index f3ee1f6c0f7..4ea55e26aee 100644 --- a/tests/components/luftdaten/test_config_flow.py +++ b/tests/components/luftdaten/test_config_flow.py @@ -1,12 +1,12 @@ """Define tests for the Luftdaten config flow.""" from datetime import timedelta +from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.components.luftdaten import DOMAIN, config_flow from homeassistant.components.luftdaten.const import CONF_SENSOR_ID from homeassistant.const import CONF_SCAN_INTERVAL, CONF_SHOW_ON_MAP -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/luftdaten/test_init.py b/tests/components/luftdaten/test_init.py index a8ea57cbb6b..ebe5f73669e 100644 --- a/tests/components/luftdaten/test_init.py +++ b/tests/components/luftdaten/test_init.py @@ -1,11 +1,11 @@ """Test the Luftdaten component setup.""" +from unittest.mock import patch + from homeassistant.components import luftdaten from homeassistant.components.luftdaten.const import CONF_SENSOR_ID, DOMAIN from homeassistant.const import CONF_SCAN_INTERVAL, CONF_SHOW_ON_MAP from homeassistant.setup import async_setup_component -from tests.async_mock import patch - async def test_config_with_sensor_passed_to_config_entry(hass): """Test that configured options for a sensor are loaded.""" diff --git a/tests/components/lutron_caseta/test_config_flow.py b/tests/components/lutron_caseta/test_config_flow.py index 4ec5f14237c..58377c8e085 100644 --- a/tests/components/lutron_caseta/test_config_flow.py +++ b/tests/components/lutron_caseta/test_config_flow.py @@ -1,7 +1,13 @@ """Test the Lutron Caseta config flow.""" -from pylutron_caseta.smartbridge import Smartbridge +import asyncio +import ssl +from unittest.mock import AsyncMock, patch -from homeassistant import config_entries, data_entry_flow +from pylutron_caseta.pairing import PAIR_CA, PAIR_CERT, PAIR_KEY +from pylutron_caseta.smartbridge import Smartbridge +import pytest + +from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.lutron_caseta import DOMAIN import homeassistant.components.lutron_caseta.config_flow as CasetaConfigFlow from homeassistant.components.lutron_caseta.const import ( @@ -11,11 +17,25 @@ from homeassistant.components.lutron_caseta.const import ( ERROR_CANNOT_CONNECT, STEP_IMPORT_FAILED, ) +from homeassistant.components.zeroconf import ATTR_HOSTNAME from homeassistant.const import CONF_HOST -from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry +EMPTY_MOCK_CONFIG_ENTRY = { + CONF_HOST: "", + CONF_KEYFILE: "", + CONF_CERTFILE: "", + CONF_CA_CERTS: "", +} + + +MOCK_ASYNC_PAIR_SUCCESS = { + PAIR_KEY: "mock_key", + PAIR_CERT: "mock_cert", + PAIR_CA: "mock_ca", +} + class MockBridge: """Mock Lutron bridge that emulates configured connected status.""" @@ -104,21 +124,34 @@ async def test_bridge_cannot_connect(hass): async def test_bridge_cannot_connect_unknown_error(hass): """Test checking for connection and encountering an unknown error.""" - entry_mock_data = { - CONF_HOST: "", - CONF_KEYFILE: "", - CONF_CERTFILE: "", - CONF_CA_CERTS: "", - } - with patch.object(Smartbridge, "create_tls") as create_tls: mock_bridge = MockBridge() - mock_bridge.connect = AsyncMock(side_effect=Exception()) + mock_bridge.connect = AsyncMock(side_effect=asyncio.TimeoutError) create_tls.return_value = mock_bridge result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data=entry_mock_data, + data=EMPTY_MOCK_CONFIG_ENTRY, + ) + + assert result["type"] == "form" + assert result["step_id"] == STEP_IMPORT_FAILED + assert result["errors"] == {"base": ERROR_CANNOT_CONNECT} + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == CasetaConfigFlow.ABORT_REASON_CANNOT_CONNECT + + +async def test_bridge_invalid_ssl_error(hass): + """Test checking for connection and encountering invalid ssl certs.""" + + with patch.object(Smartbridge, "create_tls", side_effect=ssl.SSLError): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=EMPTY_MOCK_CONFIG_ENTRY, ) assert result["type"] == "form" @@ -157,3 +190,337 @@ async def test_duplicate_bridge_import(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == CasetaConfigFlow.ABORT_REASON_ALREADY_CONFIGURED assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_already_configured_with_ignored(hass): + """Test ignored entries do not break checking for existing entries.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + config_entry = MockConfigEntry(domain=DOMAIN, data={}, source="ignore") + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={ + CONF_HOST: "1.1.1.1", + CONF_KEYFILE: "", + CONF_CERTFILE: "", + CONF_CA_CERTS: "", + }, + ) + assert result["type"] == "form" + + +async def test_form_user(hass, tmpdir): + """Test we get the form and can pair.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + hass.config.config_dir = await hass.async_add_executor_job( + tmpdir.mkdir, "tls_assets" + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] is None + assert result["step_id"] == "user" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + }, + ) + await hass.async_block_till_done() + assert result2["type"] == "form" + assert result2["step_id"] == "link" + + with patch( + "homeassistant.components.lutron_caseta.config_flow.async_pair", + return_value=MOCK_ASYNC_PAIR_SUCCESS, + ), patch( + "homeassistant.components.lutron_caseta.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.lutron_caseta.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "create_entry" + assert result3["title"] == "1.1.1.1" + assert result3["data"] == { + CONF_HOST: "1.1.1.1", + CONF_KEYFILE: "lutron_caseta-1.1.1.1-key.pem", + CONF_CERTFILE: "lutron_caseta-1.1.1.1-cert.pem", + CONF_CA_CERTS: "lutron_caseta-1.1.1.1-ca.pem", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_user_pairing_fails(hass, tmpdir): + """Test we get the form and we handle pairing failure.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + hass.config.config_dir = await hass.async_add_executor_job( + tmpdir.mkdir, "tls_assets" + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] is None + assert result["step_id"] == "user" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + }, + ) + await hass.async_block_till_done() + assert result2["type"] == "form" + assert result2["step_id"] == "link" + + with patch( + "homeassistant.components.lutron_caseta.config_flow.async_pair", + side_effect=asyncio.TimeoutError, + ), patch( + "homeassistant.components.lutron_caseta.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.lutron_caseta.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "form" + assert result3["errors"] == {"base": "cannot_connect"} + assert len(mock_setup.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_form_user_reuses_existing_assets_when_pairing_again(hass, tmpdir): + """Test the tls assets saved on disk are reused when pairing again.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + hass.config.config_dir = await hass.async_add_executor_job( + tmpdir.mkdir, "tls_assets" + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] is None + assert result["step_id"] == "user" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + }, + ) + await hass.async_block_till_done() + assert result2["type"] == "form" + assert result2["step_id"] == "link" + + with patch( + "homeassistant.components.lutron_caseta.config_flow.async_pair", + return_value=MOCK_ASYNC_PAIR_SUCCESS, + ), patch( + "homeassistant.components.lutron_caseta.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.lutron_caseta.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "create_entry" + assert result3["title"] == "1.1.1.1" + assert result3["data"] == { + CONF_HOST: "1.1.1.1", + CONF_KEYFILE: "lutron_caseta-1.1.1.1-key.pem", + CONF_CERTFILE: "lutron_caseta-1.1.1.1-cert.pem", + CONF_CA_CERTS: "lutron_caseta-1.1.1.1-ca.pem", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + with patch( + "homeassistant.components.lutron_caseta.async_unload_entry", return_value=True + ) as mock_unload: + await hass.config_entries.async_remove(result3["result"].entry_id) + await hass.async_block_till_done() + + assert len(mock_unload.mock_calls) == 1 + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] is None + assert result["step_id"] == "user" + + with patch.object(Smartbridge, "create_tls") as create_tls: + create_tls.return_value = MockBridge(can_connect=True) + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "form" + assert result2["step_id"] == "link" + + with patch( + "homeassistant.components.lutron_caseta.async_setup", return_value=True + ), patch( + "homeassistant.components.lutron_caseta.async_setup_entry", + return_value=True, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "create_entry" + assert result3["title"] == "1.1.1.1" + assert result3["data"] == { + CONF_HOST: "1.1.1.1", + CONF_KEYFILE: "lutron_caseta-1.1.1.1-key.pem", + CONF_CERTFILE: "lutron_caseta-1.1.1.1-cert.pem", + CONF_CA_CERTS: "lutron_caseta-1.1.1.1-ca.pem", + } + + +async def test_zeroconf_host_already_configured(hass, tmpdir): + """Test starting a flow from discovery when the host is already configured.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + hass.config.config_dir = await hass.async_add_executor_job( + tmpdir.mkdir, "tls_assets" + ) + + config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_HOST: "1.1.1.1"}) + + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data={ + CONF_HOST: "1.1.1.1", + ATTR_HOSTNAME: "lutron-abc.local.", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_zeroconf_lutron_id_already_configured(hass): + """Test starting a flow from discovery when lutron id already configured.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: "4.5.6.7"}, unique_id="abc" + ) + + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data={ + CONF_HOST: "1.1.1.1", + ATTR_HOSTNAME: "lutron-abc.local.", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert config_entry.data[CONF_HOST] == "1.1.1.1" + + +async def test_zeroconf_not_lutron_device(hass): + """Test starting a flow from discovery when it is not a lutron device.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data={ + CONF_HOST: "1.1.1.1", + ATTR_HOSTNAME: "notlutron-abc.local.", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "not_lutron_device" + + +@pytest.mark.parametrize( + "source", (config_entries.SOURCE_ZEROCONF, config_entries.SOURCE_HOMEKIT) +) +async def test_zeroconf(hass, source, tmpdir): + """Test starting a flow from discovery.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + hass.config.config_dir = await hass.async_add_executor_job( + tmpdir.mkdir, "tls_assets" + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": source}, + data={ + CONF_HOST: "1.1.1.1", + ATTR_HOSTNAME: "lutron-abc.local.", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + assert result["step_id"] == "link" + + with patch( + "homeassistant.components.lutron_caseta.config_flow.async_pair", + return_value=MOCK_ASYNC_PAIR_SUCCESS, + ), patch( + "homeassistant.components.lutron_caseta.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.lutron_caseta.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "abc" + assert result2["data"] == { + CONF_HOST: "1.1.1.1", + CONF_KEYFILE: "lutron_caseta-abc-key.pem", + CONF_CERTFILE: "lutron_caseta-abc-cert.pem", + CONF_CA_CERTS: "lutron_caseta-abc-ca.pem", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/lutron_caseta/test_device_trigger.py b/tests/components/lutron_caseta/test_device_trigger.py new file mode 100644 index 00000000000..9370edd48bf --- /dev/null +++ b/tests/components/lutron_caseta/test_device_trigger.py @@ -0,0 +1,346 @@ +"""The tests for Lutron Caséta device triggers.""" +import pytest + +from homeassistant import setup +from homeassistant.components import automation +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) +from homeassistant.components.lutron_caseta import ( + ATTR_ACTION, + ATTR_AREA_NAME, + ATTR_BUTTON_NUMBER, + ATTR_DEVICE_NAME, + ATTR_SERIAL, + ATTR_TYPE, +) +from homeassistant.components.lutron_caseta.const import ( + BUTTON_DEVICES, + DOMAIN, + LUTRON_CASETA_BUTTON_EVENT, + MANUFACTURER, +) +from homeassistant.components.lutron_caseta.device_trigger import CONF_SUBTYPE +from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE +from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_get_device_automations, + async_mock_service, + mock_device_registry, +) + +MOCK_BUTTON_DEVICES = [ + { + "Name": "Back Hall Pico", + "ID": 2, + "Area": {"Name": "Back Hall"}, + "Buttons": [ + {"Number": 2}, + {"Number": 3}, + {"Number": 4}, + {"Number": 5}, + {"Number": 6}, + ], + "leap_name": "Back Hall_Back Hall Pico", + "type": "Pico3ButtonRaiseLower", + "model": "PJ2-3BRL-GXX-X01", + "serial": 43845548, + } +] + + +@pytest.fixture +def calls(hass): + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +async def _async_setup_lutron_with_picos(hass, device_reg): + """Setups a lutron bridge with picos.""" + await async_setup_component(hass, DOMAIN, {}) + + config_entry = MockConfigEntry(domain=DOMAIN, data={}) + config_entry.add_to_hass(hass) + dr_button_devices = {} + + for device in MOCK_BUTTON_DEVICES: + dr_device = device_reg.async_get_or_create( + name=device["leap_name"], + manufacturer=MANUFACTURER, + config_entry_id=config_entry.entry_id, + identifiers={(DOMAIN, device["serial"])}, + model=f"{device['model']} ({device[CONF_TYPE]})", + ) + dr_button_devices[dr_device.id] = device + + hass.data[DOMAIN][config_entry.entry_id] = {BUTTON_DEVICES: dr_button_devices} + + return config_entry.entry_id + + +async def test_get_triggers(hass, device_reg): + """Test we get the expected triggers from a lutron pico.""" + config_entry_id = await _async_setup_lutron_with_picos(hass, device_reg) + dr_button_devices = hass.data[DOMAIN][config_entry_id][BUTTON_DEVICES] + device_id = list(dr_button_devices)[0] + + expected_triggers = [ + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_PLATFORM: "device", + CONF_SUBTYPE: "on", + CONF_TYPE: "press", + }, + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_PLATFORM: "device", + CONF_SUBTYPE: "stop", + CONF_TYPE: "press", + }, + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_PLATFORM: "device", + CONF_SUBTYPE: "off", + CONF_TYPE: "press", + }, + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_PLATFORM: "device", + CONF_SUBTYPE: "raise", + CONF_TYPE: "press", + }, + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_PLATFORM: "device", + CONF_SUBTYPE: "lower", + CONF_TYPE: "press", + }, + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_PLATFORM: "device", + CONF_SUBTYPE: "on", + CONF_TYPE: "release", + }, + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_PLATFORM: "device", + CONF_SUBTYPE: "stop", + CONF_TYPE: "release", + }, + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_PLATFORM: "device", + CONF_SUBTYPE: "off", + CONF_TYPE: "release", + }, + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_PLATFORM: "device", + CONF_SUBTYPE: "raise", + CONF_TYPE: "release", + }, + { + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_PLATFORM: "device", + CONF_SUBTYPE: "lower", + CONF_TYPE: "release", + }, + ] + + triggers = await async_get_device_automations(hass, "trigger", device_id) + assert_lists_same(triggers, expected_triggers) + + +async def test_get_triggers_for_invalid_device_id(hass, device_reg): + """Test error raised for invalid lutron device_id.""" + config_entry_id = await _async_setup_lutron_with_picos(hass, device_reg) + + invalid_device = device_reg.async_get_or_create( + config_entry_id=config_entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + with pytest.raises(InvalidDeviceAutomationConfig): + await async_get_device_automations(hass, "trigger", invalid_device.id) + + +async def test_if_fires_on_button_event(hass, calls, device_reg): + """Test for press trigger firing.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + config_entry_id = await _async_setup_lutron_with_picos(hass, device_reg) + dr_button_devices = hass.data[DOMAIN][config_entry_id][BUTTON_DEVICES] + device_id = list(dr_button_devices)[0] + device = dr_button_devices[device_id] + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: device_id, + CONF_TYPE: "press", + CONF_SUBTYPE: "on", + }, + "action": { + "service": "test.automation", + "data_template": {"some": "test_trigger_button_press"}, + }, + }, + ] + }, + ) + + message = { + ATTR_SERIAL: device.get("serial"), + ATTR_TYPE: device.get("type"), + ATTR_BUTTON_NUMBER: 2, + ATTR_DEVICE_NAME: device["Name"], + ATTR_AREA_NAME: device.get("Area", {}).get("Name"), + ATTR_ACTION: "press", + } + hass.bus.async_fire(LUTRON_CASETA_BUTTON_EVENT, message) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data["some"] == "test_trigger_button_press" + + +async def test_validate_trigger_config_no_device(hass, calls, device_reg): + """Test for no press with no device.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: "no_device", + CONF_TYPE: "press", + CONF_SUBTYPE: "on", + }, + "action": { + "service": "test.automation", + "data_template": {"some": "test_trigger_button_press"}, + }, + }, + ] + }, + ) + message = { + ATTR_SERIAL: "123", + ATTR_TYPE: "any", + ATTR_BUTTON_NUMBER: 3, + ATTR_DEVICE_NAME: "any", + ATTR_AREA_NAME: "area", + ATTR_ACTION: "press", + } + hass.bus.async_fire(LUTRON_CASETA_BUTTON_EVENT, message) + await hass.async_block_till_done() + + assert len(calls) == 0 + + +async def test_validate_trigger_config_unknown_device(hass, calls, device_reg): + """Test for no press with an unknown device.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + config_entry_id = await _async_setup_lutron_with_picos(hass, device_reg) + dr_button_devices = hass.data[DOMAIN][config_entry_id][BUTTON_DEVICES] + device_id = list(dr_button_devices)[0] + device = dr_button_devices[device_id] + device["type"] = "unknown" + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: device_id, + CONF_TYPE: "press", + CONF_SUBTYPE: "on", + }, + "action": { + "service": "test.automation", + "data_template": {"some": "test_trigger_button_press"}, + }, + }, + ] + }, + ) + message = { + ATTR_SERIAL: "123", + ATTR_TYPE: "any", + ATTR_BUTTON_NUMBER: 3, + ATTR_DEVICE_NAME: "any", + ATTR_AREA_NAME: "area", + ATTR_ACTION: "press", + } + hass.bus.async_fire(LUTRON_CASETA_BUTTON_EVENT, message) + await hass.async_block_till_done() + + assert len(calls) == 0 + + +async def test_validate_trigger_invalid_triggers(hass, device_reg): + """Test for click_event with invalid triggers.""" + notification_calls = async_mock_service(hass, "persistent_notification", "create") + config_entry_id = await _async_setup_lutron_with_picos(hass, device_reg) + dr_button_devices = hass.data[DOMAIN][config_entry_id][BUTTON_DEVICES] + device_id = list(dr_button_devices)[0] + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: device_id, + CONF_TYPE: "press", + CONF_SUBTYPE: "on", + }, + "action": { + "service": "test.automation", + "data_template": {"some": "test_trigger_button_press"}, + }, + }, + ] + }, + ) + + assert len(notification_calls) == 1 + assert ( + "The following integrations and platforms could not be set up" + in notification_calls[0].data["message"] + ) diff --git a/tests/components/manual/test_alarm_control_panel.py b/tests/components/manual/test_alarm_control_panel.py index 9cf76ebbc8f..ee3d97e52d7 100644 --- a/tests/components/manual/test_alarm_control_panel.py +++ b/tests/components/manual/test_alarm_control_panel.py @@ -1,5 +1,6 @@ """The tests for the manual Alarm Control Panel component.""" from datetime import timedelta +from unittest.mock import MagicMock, patch from homeassistant.components import alarm_control_panel from homeassistant.components.demo import alarm_control_panel as demo @@ -17,7 +18,6 @@ from homeassistant.core import CoreState, State from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import MagicMock, patch from tests.common import async_fire_time_changed, mock_component, mock_restore_cache from tests.components.alarm_control_panel import common diff --git a/tests/components/manual_mqtt/test_alarm_control_panel.py b/tests/components/manual_mqtt/test_alarm_control_panel.py index 87a887b0751..9a98af127ea 100644 --- a/tests/components/manual_mqtt/test_alarm_control_panel.py +++ b/tests/components/manual_mqtt/test_alarm_control_panel.py @@ -1,5 +1,6 @@ """The tests for the manual_mqtt Alarm Control Panel component.""" from datetime import timedelta +from unittest.mock import patch from homeassistant.components import alarm_control_panel from homeassistant.const import ( @@ -13,7 +14,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import ( assert_setup_component, async_fire_mqtt_message, diff --git a/tests/components/marytts/test_tts.py b/tests/components/marytts/test_tts.py index ce2092dd607..6a09ff405c8 100644 --- a/tests/components/marytts/test_tts.py +++ b/tests/components/marytts/test_tts.py @@ -2,6 +2,7 @@ import asyncio import os import shutil +from unittest.mock import patch from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, @@ -12,7 +13,6 @@ import homeassistant.components.tts as tts from homeassistant.config import async_process_ha_core_config from homeassistant.setup import setup_component -from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant, mock_service diff --git a/tests/components/media_player/test_async_helpers.py b/tests/components/media_player/test_async_helpers.py index 12414801a52..dac35c71515 100644 --- a/tests/components/media_player/test_async_helpers.py +++ b/tests/components/media_player/test_async_helpers.py @@ -1,6 +1,5 @@ """The tests for the Async Media player helper functions.""" -import asyncio -import unittest +import pytest import homeassistant.components.media_player as mp from homeassistant.const import ( @@ -11,62 +10,9 @@ from homeassistant.const import ( STATE_PLAYING, ) -from tests.common import get_test_home_assistant - -class AsyncMediaPlayer(mp.MediaPlayerEntity): - """Async media player test class.""" - - def __init__(self, hass): - """Initialize the test media player.""" - self.hass = hass - self._volume = 0 - self._state = STATE_OFF - - @property - def state(self): - """State of the player.""" - return self._state - - @property - def volume_level(self): - """Volume level of the media player (0..1).""" - return self._volume - - @property - def supported_features(self): - """Flag media player features that are supported.""" - return ( - mp.const.SUPPORT_VOLUME_SET - | mp.const.SUPPORT_PLAY - | mp.const.SUPPORT_PAUSE - | mp.const.SUPPORT_TURN_OFF - | mp.const.SUPPORT_TURN_ON - ) - - async def async_set_volume_level(self, volume): - """Set volume level, range 0..1.""" - self._volume = volume - - async def async_media_play(self): - """Send play command.""" - self._state = STATE_PLAYING - - async def async_media_pause(self): - """Send pause command.""" - self._state = STATE_PAUSED - - async def async_turn_on(self): - """Turn the media player on.""" - self._state = STATE_ON - - async def async_turn_off(self): - """Turn the media player off.""" - self._state = STATE_OFF - - -class SyncMediaPlayer(mp.MediaPlayerEntity): - """Sync media player test class.""" +class ExtendedMediaPlayer(mp.MediaPlayerEntity): + """Media player test class.""" def __init__(self, hass): """Initialize the test media player.""" @@ -103,12 +49,20 @@ class SyncMediaPlayer(mp.MediaPlayerEntity): def volume_up(self): """Turn volume up for media player.""" if self.volume_level < 1: - self.set_volume_level(min(1, self.volume_level + 0.2)) + self.set_volume_level(min(1, self.volume_level + 0.1)) def volume_down(self): """Turn volume down for media player.""" if self.volume_level > 0: - self.set_volume_level(max(0, self.volume_level - 0.2)) + self.set_volume_level(max(0, self.volume_level - 0.1)) + + def media_play(self): + """Play the media player.""" + self._state = STATE_PLAYING + + def media_pause(self): + """Plause the media player.""" + self._state = STATE_PAUSED def media_play_pause(self): """Play or pause the media player.""" @@ -117,6 +71,14 @@ class SyncMediaPlayer(mp.MediaPlayerEntity): else: self._state = STATE_PLAYING + def turn_on(self): + """Turn on state.""" + self._state = STATE_ON + + def turn_off(self): + """Turn off state.""" + self._state = STATE_OFF + def toggle(self): """Toggle the power on the media player.""" if self._state in [STATE_OFF, STATE_IDLE]: @@ -124,136 +86,105 @@ class SyncMediaPlayer(mp.MediaPlayerEntity): else: self._state = STATE_OFF - async def async_media_play_pause(self): - """Create a coroutine to wrap the future returned by ABC. - This allows the run_coroutine_threadsafe helper to be used. - """ - await super().async_media_play_pause() +class SimpleMediaPlayer(mp.MediaPlayerEntity): + """Media player test class.""" - async def async_toggle(self): - """Create a coroutine to wrap the future returned by ABC. + def __init__(self, hass): + """Initialize the test media player.""" + self.hass = hass + self._volume = 0 + self._state = STATE_OFF - This allows the run_coroutine_threadsafe helper to be used. - """ - await super().async_toggle() + @property + def state(self): + """State of the player.""" + return self._state + + @property + def volume_level(self): + """Volume level of the media player (0..1).""" + return self._volume + + @property + def supported_features(self): + """Flag media player features that are supported.""" + return ( + mp.const.SUPPORT_VOLUME_SET + | mp.const.SUPPORT_VOLUME_STEP + | mp.const.SUPPORT_PLAY + | mp.const.SUPPORT_PAUSE + | mp.const.SUPPORT_TURN_OFF + | mp.const.SUPPORT_TURN_ON + ) + + def set_volume_level(self, volume): + """Set volume level, range 0..1.""" + self._volume = volume + + def media_play(self): + """Play the media player.""" + self._state = STATE_PLAYING + + def media_pause(self): + """Plause the media player.""" + self._state = STATE_PAUSED + + def turn_on(self): + """Turn on state.""" + self._state = STATE_ON + + def turn_off(self): + """Turn off state.""" + self._state = STATE_OFF -class TestAsyncMediaPlayer(unittest.TestCase): - """Test the media_player module.""" - - def setUp(self): # pylint: disable=invalid-name - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.player = AsyncMediaPlayer(self.hass) - self.addCleanup(self.tear_down_cleanup) - - def tear_down_cleanup(self): - """Shut down test instance.""" - self.hass.stop() - - def test_volume_up(self): - """Test the volume_up helper function.""" - assert self.player.volume_level == 0 - asyncio.run_coroutine_threadsafe( - self.player.async_set_volume_level(0.5), self.hass.loop - ).result() - assert self.player.volume_level == 0.5 - asyncio.run_coroutine_threadsafe( - self.player.async_volume_up(), self.hass.loop - ).result() - assert self.player.volume_level == 0.6 - - def test_volume_down(self): - """Test the volume_down helper function.""" - assert self.player.volume_level == 0 - asyncio.run_coroutine_threadsafe( - self.player.async_set_volume_level(0.5), self.hass.loop - ).result() - assert self.player.volume_level == 0.5 - asyncio.run_coroutine_threadsafe( - self.player.async_volume_down(), self.hass.loop - ).result() - assert self.player.volume_level == 0.4 - - def test_media_play_pause(self): - """Test the media_play_pause helper function.""" - assert self.player.state == STATE_OFF - asyncio.run_coroutine_threadsafe( - self.player.async_media_play_pause(), self.hass.loop - ).result() - assert self.player.state == STATE_PLAYING - asyncio.run_coroutine_threadsafe( - self.player.async_media_play_pause(), self.hass.loop - ).result() - assert self.player.state == STATE_PAUSED - - def test_toggle(self): - """Test the toggle helper function.""" - assert self.player.state == STATE_OFF - asyncio.run_coroutine_threadsafe( - self.player.async_toggle(), self.hass.loop - ).result() - assert self.player.state == STATE_ON - asyncio.run_coroutine_threadsafe( - self.player.async_toggle(), self.hass.loop - ).result() - assert self.player.state == STATE_OFF +@pytest.fixture(params=[ExtendedMediaPlayer, SimpleMediaPlayer]) +def player(hass, request): + """Return a media player.""" + return request.param(hass) -class TestSyncMediaPlayer(unittest.TestCase): - """Test the media_player module.""" +async def test_volume_up(player): + """Test the volume_up and set volume methods.""" + assert player.volume_level == 0 + await player.async_set_volume_level(0.5) + assert player.volume_level == 0.5 + await player.async_volume_up() + assert player.volume_level == 0.6 - def setUp(self): # pylint: disable=invalid-name - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.player = SyncMediaPlayer(self.hass) - self.addCleanup(self.tear_down_cleanup) - def tear_down_cleanup(self): - """Shut down test instance.""" - self.hass.stop() +async def test_volume_down(player): + """Test the volume_down and set volume methods.""" + assert player.volume_level == 0 + await player.async_set_volume_level(0.5) + assert player.volume_level == 0.5 + await player.async_volume_down() + assert player.volume_level == 0.4 - def test_volume_up(self): - """Test the volume_up helper function.""" - assert self.player.volume_level == 0 - self.player.set_volume_level(0.5) - assert self.player.volume_level == 0.5 - asyncio.run_coroutine_threadsafe( - self.player.async_volume_up(), self.hass.loop - ).result() - assert self.player.volume_level == 0.7 - def test_volume_down(self): - """Test the volume_down helper function.""" - assert self.player.volume_level == 0 - self.player.set_volume_level(0.5) - assert self.player.volume_level == 0.5 - asyncio.run_coroutine_threadsafe( - self.player.async_volume_down(), self.hass.loop - ).result() - assert self.player.volume_level == 0.3 +async def test_media_play_pause(player): + """Test the media_play_pause method.""" + assert player.state == STATE_OFF + await player.async_media_play_pause() + assert player.state == STATE_PLAYING + await player.async_media_play_pause() + assert player.state == STATE_PAUSED - def test_media_play_pause(self): - """Test the media_play_pause helper function.""" - assert self.player.state == STATE_OFF - asyncio.run_coroutine_threadsafe( - self.player.async_media_play_pause(), self.hass.loop - ).result() - assert self.player.state == STATE_PLAYING - asyncio.run_coroutine_threadsafe( - self.player.async_media_play_pause(), self.hass.loop - ).result() - assert self.player.state == STATE_PAUSED - def test_toggle(self): - """Test the toggle helper function.""" - assert self.player.state == STATE_OFF - asyncio.run_coroutine_threadsafe( - self.player.async_toggle(), self.hass.loop - ).result() - assert self.player.state == STATE_ON - asyncio.run_coroutine_threadsafe( - self.player.async_toggle(), self.hass.loop - ).result() - assert self.player.state == STATE_OFF +async def test_turn_on_off(player): + """Test the turn on and turn off methods.""" + assert player.state == STATE_OFF + await player.async_turn_on() + assert player.state == STATE_ON + await player.async_turn_off() + assert player.state == STATE_OFF + + +async def test_toggle(player): + """Test the toggle method.""" + assert player.state == STATE_OFF + await player.async_toggle() + assert player.state == STATE_ON + await player.async_toggle() + assert player.state == STATE_OFF diff --git a/tests/components/media_player/test_init.py b/tests/components/media_player/test_init.py index 9434fb1a411..98b54ace01f 100644 --- a/tests/components/media_player/test_init.py +++ b/tests/components/media_player/test_init.py @@ -1,12 +1,11 @@ """Test the base functions of the media player.""" import base64 +from unittest.mock import patch from homeassistant.components import media_player from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.setup import async_setup_component -from tests.async_mock import patch - async def test_get_image(hass, hass_ws_client, caplog): """Test get image via WS command.""" diff --git a/tests/components/media_source/test_init.py b/tests/components/media_source/test_init.py index a891fb0d11d..0dda9f67fbe 100644 --- a/tests/components/media_source/test_init.py +++ b/tests/components/media_source/test_init.py @@ -1,4 +1,6 @@ """Test Media Source initialization.""" +from unittest.mock import patch + import pytest from homeassistant.components import media_source @@ -8,8 +10,6 @@ from homeassistant.components.media_source import const from homeassistant.components.media_source.error import Unresolvable from homeassistant.setup import async_setup_component -from tests.async_mock import patch - async def test_is_media_source_id(): """Test media source validation.""" diff --git a/tests/components/media_source/test_local_source.py b/tests/components/media_source/test_local_source.py index e3e2a3f1617..ad10df7cfd3 100644 --- a/tests/components/media_source/test_local_source.py +++ b/tests/components/media_source/test_local_source.py @@ -23,7 +23,7 @@ async def test_async_browse_media(hass): await media_source.async_browse_media( hass, f"{const.URI_SCHEME}{const.DOMAIN}/local/test/not/exist" ) - assert str(excinfo.value) == "Path does not exist." + assert str(excinfo.value) == "Invalid path." # Test browse file with pytest.raises(media_source.BrowseError) as excinfo: diff --git a/tests/components/melcloud/test_config_flow.py b/tests/components/melcloud/test_config_flow.py index 1fca3ac877e..dbf7a455791 100644 --- a/tests/components/melcloud/test_config_flow.py +++ b/tests/components/melcloud/test_config_flow.py @@ -1,5 +1,6 @@ """Test the MELCloud config flow.""" import asyncio +from unittest.mock import patch from aiohttp import ClientError, ClientResponseError import pymelcloud @@ -9,7 +10,6 @@ from homeassistant import config_entries from homeassistant.components.melcloud.const import DOMAIN from homeassistant.const import HTTP_FORBIDDEN, HTTP_INTERNAL_SERVER_ERROR -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/melissa/test_climate.py b/tests/components/melissa/test_climate.py index 874cc29ab7a..ccb5f951fa2 100644 --- a/tests/components/melissa/test_climate.py +++ b/tests/components/melissa/test_climate.py @@ -1,5 +1,6 @@ """Test for Melissa climate component.""" import json +from unittest.mock import AsyncMock, Mock, patch from homeassistant.components.climate.const import ( HVAC_MODE_COOL, @@ -15,7 +16,6 @@ from homeassistant.components.melissa import DATA_MELISSA, climate as melissa from homeassistant.components.melissa.climate import MelissaClimate from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS -from tests.async_mock import AsyncMock, Mock, patch from tests.common import load_fixture _SERIAL = "12345678" diff --git a/tests/components/melissa/test_init.py b/tests/components/melissa/test_init.py index b9e09a5d769..8ac48cbfd5d 100644 --- a/tests/components/melissa/test_init.py +++ b/tests/components/melissa/test_init.py @@ -1,7 +1,7 @@ """The test for the Melissa Climate component.""" -from homeassistant.components import melissa +from unittest.mock import AsyncMock, patch -from tests.async_mock import AsyncMock, patch +from homeassistant.components import melissa VALID_CONFIG = {"melissa": {"username": "********", "password": "********"}} diff --git a/tests/components/met/__init__.py b/tests/components/met/__init__.py index c238fec4cb7..13b186f3b47 100644 --- a/tests/components/met/__init__.py +++ b/tests/components/met/__init__.py @@ -1,8 +1,9 @@ """Tests for Met.no.""" +from unittest.mock import patch + from homeassistant.components.met.const import DOMAIN from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/met/conftest.py b/tests/components/met/conftest.py index 164a8498465..e6b975023d1 100644 --- a/tests/components/met/conftest.py +++ b/tests/components/met/conftest.py @@ -1,7 +1,7 @@ """Fixtures for Met weather testing.""" -import pytest +from unittest.mock import AsyncMock, patch -from tests.async_mock import AsyncMock, patch +import pytest @pytest.fixture diff --git a/tests/components/met/test_config_flow.py b/tests/components/met/test_config_flow.py index 52959428917..622475e8376 100644 --- a/tests/components/met/test_config_flow.py +++ b/tests/components/met/test_config_flow.py @@ -1,10 +1,11 @@ """Tests for Met.no config flow.""" +from unittest.mock import patch + import pytest from homeassistant.components.met.const import DOMAIN, HOME_LOCATION_NAME from homeassistant.const import CONF_ELEVATION, CONF_LATITUDE, CONF_LONGITUDE -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/meteo_france/conftest.py b/tests/components/meteo_france/conftest.py index 06a65b6ba87..0b62a03a53c 100644 --- a/tests/components/meteo_france/conftest.py +++ b/tests/components/meteo_france/conftest.py @@ -1,7 +1,7 @@ """Meteo-France generic test utils.""" -import pytest +from unittest.mock import patch -from tests.async_mock import patch +import pytest @pytest.fixture(autouse=True) diff --git a/tests/components/meteo_france/test_config_flow.py b/tests/components/meteo_france/test_config_flow.py index 6c1d9312f91..0fbe1b5e135 100644 --- a/tests/components/meteo_france/test_config_flow.py +++ b/tests/components/meteo_france/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for the Meteo-France config flow.""" +from unittest.mock import patch + from meteofrance_api.model import Place import pytest @@ -13,7 +15,6 @@ from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE, CONF_MODE from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import patch from tests.common import MockConfigEntry CITY_1_POSTAL = "74220" diff --git a/tests/components/metoffice/conftest.py b/tests/components/metoffice/conftest.py index 9538c7a8668..78d7d2c2eee 100644 --- a/tests/components/metoffice/conftest.py +++ b/tests/components/metoffice/conftest.py @@ -1,9 +1,9 @@ """Fixtures for Met Office weather integration tests.""" +from unittest.mock import patch + from datapoint.exceptions import APIException import pytest -from tests.async_mock import patch - @pytest.fixture() def mock_simple_manager_fail(): diff --git a/tests/components/metoffice/test_config_flow.py b/tests/components/metoffice/test_config_flow.py index 3f248704fa1..f0023b0d8d5 100644 --- a/tests/components/metoffice/test_config_flow.py +++ b/tests/components/metoffice/test_config_flow.py @@ -1,5 +1,6 @@ """Test the National Weather Service (NWS) config flow.""" import json +from unittest.mock import patch from homeassistant import config_entries, setup from homeassistant.components.metoffice.const import DOMAIN @@ -12,7 +13,6 @@ from .const import ( TEST_SITE_NAME_WAVERTREE, ) -from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture diff --git a/tests/components/metoffice/test_sensor.py b/tests/components/metoffice/test_sensor.py index 43dbf3f75e0..43f460056f9 100644 --- a/tests/components/metoffice/test_sensor.py +++ b/tests/components/metoffice/test_sensor.py @@ -1,5 +1,6 @@ """The tests for the Met Office sensor component.""" import json +from unittest.mock import patch from homeassistant.components.metoffice.const import ATTRIBUTION, DOMAIN @@ -15,7 +16,6 @@ from .const import ( WAVERTREE_SENSOR_RESULTS, ) -from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture diff --git a/tests/components/metoffice/test_weather.py b/tests/components/metoffice/test_weather.py index f1530021fcf..18edbc4a972 100644 --- a/tests/components/metoffice/test_weather.py +++ b/tests/components/metoffice/test_weather.py @@ -1,6 +1,7 @@ """The tests for the Met Office sensor component.""" from datetime import timedelta import json +from unittest.mock import patch from homeassistant.components.metoffice.const import DOMAIN from homeassistant.const import STATE_UNAVAILABLE @@ -13,7 +14,6 @@ from .const import ( WAVERTREE_SENSOR_RESULTS, ) -from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture diff --git a/tests/components/mfi/test_sensor.py b/tests/components/mfi/test_sensor.py index ad23039237c..610aa91cd4c 100644 --- a/tests/components/mfi/test_sensor.py +++ b/tests/components/mfi/test_sensor.py @@ -1,4 +1,6 @@ """The tests for the mFi sensor platform.""" +import unittest.mock as mock + from mficlient.client import FailedToLogin import pytest import requests @@ -8,8 +10,6 @@ import homeassistant.components.sensor as sensor_component from homeassistant.const import TEMP_CELSIUS from homeassistant.setup import async_setup_component -import tests.async_mock as mock - PLATFORM = mfi COMPONENT = sensor_component THING = "sensor" diff --git a/tests/components/mfi/test_switch.py b/tests/components/mfi/test_switch.py index b11dcdccb6e..0409a4f387a 100644 --- a/tests/components/mfi/test_switch.py +++ b/tests/components/mfi/test_switch.py @@ -1,12 +1,12 @@ """The tests for the mFi switch platform.""" +import unittest.mock as mock + import pytest import homeassistant.components.mfi.switch as mfi import homeassistant.components.switch as switch_component from homeassistant.setup import async_setup_component -import tests.async_mock as mock - PLATFORM = mfi COMPONENT = switch_component THING = "switch" diff --git a/tests/components/mhz19/test_sensor.py b/tests/components/mhz19/test_sensor.py index 6305f86cedc..5b6e1e9fa37 100644 --- a/tests/components/mhz19/test_sensor.py +++ b/tests/components/mhz19/test_sensor.py @@ -1,4 +1,6 @@ """Tests for MH-Z19 sensor.""" +from unittest.mock import DEFAULT, Mock, patch + from pmsensor import co2sensor from pmsensor.co2sensor import read_mh_z19_with_temperature @@ -11,7 +13,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import DEFAULT, Mock, patch from tests.common import assert_setup_component diff --git a/tests/components/microsoft_face/test_init.py b/tests/components/microsoft_face/test_init.py index abd35eca3e1..191c59e556f 100644 --- a/tests/components/microsoft_face/test_init.py +++ b/tests/components/microsoft_face/test_init.py @@ -1,5 +1,6 @@ """The tests for the microsoft face platform.""" import asyncio +from unittest.mock import patch from homeassistant.components import camera, microsoft_face as mf from homeassistant.components.microsoft_face import ( @@ -17,7 +18,6 @@ from homeassistant.components.microsoft_face import ( from homeassistant.const import ATTR_NAME from homeassistant.setup import setup_component -from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant, load_fixture diff --git a/tests/components/mikrotik/test_config_flow.py b/tests/components/mikrotik/test_config_flow.py index ad3a27d724a..c884315f400 100644 --- a/tests/components/mikrotik/test_config_flow.py +++ b/tests/components/mikrotik/test_config_flow.py @@ -1,5 +1,6 @@ """Test Mikrotik setup process.""" from datetime import timedelta +from unittest.mock import patch import librouteros import pytest @@ -15,7 +16,6 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) -from tests.async_mock import patch from tests.common import MockConfigEntry DEMO_USER_INPUT = { diff --git a/tests/components/mikrotik/test_device_tracker.py b/tests/components/mikrotik/test_device_tracker.py index 4e8ea93ab30..d4151be0add 100644 --- a/tests/components/mikrotik/test_device_tracker.py +++ b/tests/components/mikrotik/test_device_tracker.py @@ -48,6 +48,11 @@ async def test_device_trackers(hass, legacy_patchable_time): device_1 = hass.states.get("device_tracker.device_1") assert device_1 is not None assert device_1.state == "home" + assert device_1.attributes["ip"] == "0.0.0.1" + assert "ip_address" not in device_1.attributes + assert device_1.attributes["mac"] == "00:00:00:00:00:01" + assert device_1.attributes["host_name"] == "Device_1" + assert "mac_address" not in device_1.attributes device_2 = hass.states.get("device_tracker.device_2") assert device_2 is None @@ -61,6 +66,11 @@ async def test_device_trackers(hass, legacy_patchable_time): device_2 = hass.states.get("device_tracker.device_2") assert device_2 is not None assert device_2.state == "home" + assert device_2.attributes["ip"] == "0.0.0.2" + assert "ip_address" not in device_2.attributes + assert device_2.attributes["mac"] == "00:00:00:00:00:02" + assert "mac_address" not in device_2.attributes + assert device_2.attributes["host_name"] == "Device_2" # test state remains home if last_seen consider_home_interval del WIRELESS_DATA[1] # device 2 is removed from wireless list diff --git a/tests/components/mikrotik/test_hub.py b/tests/components/mikrotik/test_hub.py index ecfb9add717..27c53786519 100644 --- a/tests/components/mikrotik/test_hub.py +++ b/tests/components/mikrotik/test_hub.py @@ -1,4 +1,6 @@ """Test Mikrotik hub.""" +from unittest.mock import patch + import librouteros from homeassistant import config_entries @@ -6,7 +8,6 @@ from homeassistant.components import mikrotik from . import ARP_DATA, DHCP_DATA, MOCK_DATA, MOCK_OPTIONS, WIRELESS_DATA -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/mikrotik/test_init.py b/tests/components/mikrotik/test_init.py index 30df96e89a0..281b70e36be 100644 --- a/tests/components/mikrotik/test_init.py +++ b/tests/components/mikrotik/test_init.py @@ -1,10 +1,11 @@ """Test Mikrotik setup process.""" +from unittest.mock import AsyncMock, Mock, patch + from homeassistant.components import mikrotik from homeassistant.setup import async_setup_component from . import MOCK_DATA -from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/mill/test_config_flow.py b/tests/components/mill/test_config_flow.py index ff95af44ff5..ee565d32211 100644 --- a/tests/components/mill/test_config_flow.py +++ b/tests/components/mill/test_config_flow.py @@ -1,10 +1,11 @@ """Tests for Mill config flow.""" +from unittest.mock import patch + import pytest from homeassistant.components.mill.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/min_max/test_sensor.py b/tests/components/min_max/test_sensor.py index 0d3a2e8fdcd..93360d57786 100644 --- a/tests/components/min_max/test_sensor.py +++ b/tests/components/min_max/test_sensor.py @@ -1,6 +1,7 @@ """The test for the min/max sensor platform.""" from os import path import statistics +from unittest.mock import patch from homeassistant import config as hass_config from homeassistant.components.min_max import DOMAIN @@ -15,8 +16,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import patch - VALUES = [17, 20, 15.3] COUNT = len(VALUES) MIN_VALUE = min(VALUES) diff --git a/tests/components/minecraft_server/test_config_flow.py b/tests/components/minecraft_server/test_config_flow.py index 2f8ae5ff0bf..2f3b7781ecf 100644 --- a/tests/components/minecraft_server/test_config_flow.py +++ b/tests/components/minecraft_server/test_config_flow.py @@ -1,6 +1,7 @@ """Test the Minecraft Server config flow.""" import asyncio +from unittest.mock import patch import aiodns from mcstatus.pinger import PingResponse @@ -19,7 +20,6 @@ from homeassistant.data_entry_flow import ( ) from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/minio/test_minio.py b/tests/components/minio/test_minio.py index 8cde32f7870..de66ee2fafa 100644 --- a/tests/components/minio/test_minio.py +++ b/tests/components/minio/test_minio.py @@ -1,6 +1,7 @@ """Tests for Minio Hass related code.""" import asyncio import json +from unittest.mock import MagicMock, call, patch import pytest @@ -18,7 +19,6 @@ from homeassistant.components.minio import ( from homeassistant.core import callback from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, call, patch from tests.components.minio.common import TEST_EVENT diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index b56b56f239a..831c8250d7a 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -1,4 +1,6 @@ """Webhook tests for mobile_app.""" +from unittest.mock import patch + import pytest from homeassistant.components.camera import SUPPORT_STREAM as CAMERA_SUPPORT_STREAM @@ -11,7 +13,6 @@ from homeassistant.setup import async_setup_component from .const import CALL_SERVICE, FIRE_EVENT, REGISTER_CLEARTEXT, RENDER_TEMPLATE, UPDATE -from tests.async_mock import patch from tests.common import async_mock_service diff --git a/tests/components/mochad/test_light.py b/tests/components/mochad/test_light.py index 9d484b2db65..0cb1b022a60 100644 --- a/tests/components/mochad/test_light.py +++ b/tests/components/mochad/test_light.py @@ -1,13 +1,13 @@ """The tests for the mochad light platform.""" +import unittest.mock as mock + import pytest from homeassistant.components import light from homeassistant.components.mochad import light as mochad from homeassistant.setup import async_setup_component -import tests.async_mock as mock - @pytest.fixture(autouse=True) def pymochad_mock(): diff --git a/tests/components/mochad/test_switch.py b/tests/components/mochad/test_switch.py index e8d58233c40..218248c3442 100644 --- a/tests/components/mochad/test_switch.py +++ b/tests/components/mochad/test_switch.py @@ -1,12 +1,12 @@ """The tests for the mochad switch platform.""" +import unittest.mock as mock + import pytest from homeassistant.components import switch from homeassistant.components.mochad import switch as mochad from homeassistant.setup import async_setup_component -import tests.async_mock as mock - @pytest.fixture(autouse=True) def pymochad_mock(): diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index ce6e6585512..e3a707b7fc9 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -1,5 +1,6 @@ """The tests for the Modbus sensor component.""" from unittest import mock +from unittest.mock import patch import pytest @@ -14,7 +15,6 @@ from homeassistant.const import CONF_PLATFORM, CONF_SCAN_INTERVAL from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import async_fire_time_changed diff --git a/tests/components/mold_indicator/test_sensor.py b/tests/components/mold_indicator/test_sensor.py index a923d4a2cf3..2496bddc57e 100644 --- a/tests/components/mold_indicator/test_sensor.py +++ b/tests/components/mold_indicator/test_sensor.py @@ -165,8 +165,8 @@ async def test_calculation(hass): # assert dewpoint dewpoint = moldind.attributes.get(ATTR_DEWPOINT) assert dewpoint - assert dewpoint > 9.25 - assert dewpoint < 9.26 + assert dewpoint > 9.2 + assert dewpoint < 9.3 # assert temperature estimation esttemp = moldind.attributes.get(ATTR_CRITICAL_TEMP) @@ -246,8 +246,8 @@ async def test_unknown_sensor(hass): dewpoint = moldind.attributes.get(ATTR_DEWPOINT) assert dewpoint - assert dewpoint > 4.58 - assert dewpoint < 4.59 + assert dewpoint > 4.5 + assert dewpoint < 4.6 esttemp = moldind.attributes.get(ATTR_CRITICAL_TEMP) assert esttemp diff --git a/tests/components/monoprice/test_config_flow.py b/tests/components/monoprice/test_config_flow.py index f3530bb2c75..0b954cb6e34 100644 --- a/tests/components/monoprice/test_config_flow.py +++ b/tests/components/monoprice/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Monoprice 6-Zone Amplifier config flow.""" +from unittest.mock import patch + from serial import SerialException from homeassistant import config_entries, data_entry_flow, setup @@ -11,7 +13,6 @@ from homeassistant.components.monoprice.const import ( ) from homeassistant.const import CONF_PORT -from tests.async_mock import patch from tests.common import MockConfigEntry CONFIG = { diff --git a/tests/components/monoprice/test_media_player.py b/tests/components/monoprice/test_media_player.py index 41a33fd095b..d7b505b5279 100644 --- a/tests/components/monoprice/test_media_player.py +++ b/tests/components/monoprice/test_media_player.py @@ -1,5 +1,6 @@ """The tests for Monoprice Media player platform.""" from collections import defaultdict +from unittest.mock import patch from serial import SerialException @@ -34,7 +35,6 @@ from homeassistant.const import ( ) from homeassistant.helpers.entity_component import async_update_entity -from tests.async_mock import patch from tests.common import MockConfigEntry MOCK_CONFIG = {CONF_PORT: "fake port", CONF_SOURCES: {"1": "one", "3": "three"}} diff --git a/tests/components/moon/test_sensor.py b/tests/components/moon/test_sensor.py index 234a8a1e34d..8a0269ba9fe 100644 --- a/tests/components/moon/test_sensor.py +++ b/tests/components/moon/test_sensor.py @@ -1,5 +1,6 @@ """The test for the moon sensor platform.""" from datetime import datetime +from unittest.mock import patch from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, @@ -9,8 +10,6 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch - DAY1 = datetime(2017, 1, 1, 1, tzinfo=dt_util.UTC) DAY2 = datetime(2017, 1, 18, 1, tzinfo=dt_util.UTC) diff --git a/tests/components/motion_blinds/test_config_flow.py b/tests/components/motion_blinds/test_config_flow.py index 4a25026959c..18592421249 100644 --- a/tests/components/motion_blinds/test_config_flow.py +++ b/tests/components/motion_blinds/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Motion Blinds config flow.""" import socket +from unittest.mock import Mock, patch import pytest @@ -8,8 +9,6 @@ from homeassistant.components.motion_blinds.config_flow import DEFAULT_GATEWAY_N from homeassistant.components.motion_blinds.const import DOMAIN from homeassistant.const import CONF_API_KEY, CONF_HOST -from tests.async_mock import Mock, patch - TEST_HOST = "1.2.3.4" TEST_HOST2 = "5.6.7.8" TEST_API_KEY = "12ab345c-d67e-8f" diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 524448e5839..cfb59ba27a7 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -1,6 +1,7 @@ """The tests the MQTT alarm control panel component.""" import copy import json +from unittest.mock import patch import pytest @@ -43,7 +44,6 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.async_mock import patch from tests.common import assert_setup_component, async_fire_mqtt_message from tests.components.alarm_control_panel import common diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index d4c837de023..bf88b7901e1 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -2,6 +2,7 @@ import copy from datetime import datetime, timedelta import json +from unittest.mock import patch import pytest @@ -40,7 +41,6 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.async_mock import patch from tests.common import async_fire_mqtt_message, async_fire_time_changed DEFAULT_CONFIG = { diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 1f3acee119e..13c15796cfd 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -1,5 +1,6 @@ """The tests for mqtt camera component.""" import json +from unittest.mock import patch import pytest @@ -30,7 +31,6 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.async_mock import patch from tests.common import async_fire_mqtt_message DEFAULT_CONFIG = { diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 0a9c1dc6104..546c112b153 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -1,6 +1,7 @@ """The tests for the mqtt climate component.""" import copy import json +from unittest.mock import call, patch import pytest import voluptuous as vol @@ -49,7 +50,6 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.async_mock import call, patch from tests.common import async_fire_mqtt_message from tests.components.climate import common @@ -644,8 +644,8 @@ async def test_custom_availability_payload(hass, mqtt_mock): ) -async def test_set_target_temperature_low_high_with_templates(hass, mqtt_mock, caplog): - """Test setting of temperature high/low templates.""" +async def test_get_target_temperature_low_high_with_templates(hass, mqtt_mock, caplog): + """Test getting temperature high/low with templates.""" config = copy.deepcopy(DEFAULT_CONFIG) config["climate"]["temperature_low_state_topic"] = "temperature-state" config["climate"]["temperature_high_state_topic"] = "temperature-state" @@ -678,8 +678,8 @@ async def test_set_target_temperature_low_high_with_templates(hass, mqtt_mock, c assert state.attributes.get("target_temp_high") == 1032 -async def test_set_with_templates(hass, mqtt_mock, caplog): - """Test setting of new fan mode in pessimistic mode.""" +async def test_get_with_templates(hass, mqtt_mock, caplog): + """Test getting various attributes with templates.""" config = copy.deepcopy(DEFAULT_CONFIG) # By default, just unquote the JSON-strings config["climate"]["value_template"] = "{{ value_json }}" @@ -782,6 +782,80 @@ async def test_set_with_templates(hass, mqtt_mock, caplog): assert state.attributes.get("hvac_action") == "cool" +async def test_set_with_templates(hass, mqtt_mock, caplog): + """Test setting various attributes with templates.""" + config = copy.deepcopy(DEFAULT_CONFIG) + # Create simple templates + config["climate"]["fan_mode_command_template"] = "fan_mode: {{ value }}" + config["climate"]["hold_command_template"] = "hold: {{ value }}" + config["climate"]["mode_command_template"] = "mode: {{ value }}" + config["climate"]["swing_mode_command_template"] = "swing_mode: {{ value }}" + config["climate"]["temperature_command_template"] = "temp: {{ value }}" + config["climate"]["temperature_high_command_template"] = "temp_hi: {{ value }}" + config["climate"]["temperature_low_command_template"] = "temp_lo: {{ value }}" + + assert await async_setup_component(hass, CLIMATE_DOMAIN, config) + await hass.async_block_till_done() + + # Fan Mode + await common.async_set_fan_mode(hass, "high", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with( + "fan-mode-topic", "fan_mode: high", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("fan_mode") == "high" + + # Hold Mode + await common.async_set_preset_mode(hass, PRESET_ECO, ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with("hold-topic", "hold: eco", 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("preset_mode") == PRESET_ECO + + # Mode + await common.async_set_hvac_mode(hass, "cool", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with( + "mode-topic", "mode: cool", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get(ENTITY_CLIMATE) + assert state.state == "cool" + + # Swing Mode + await common.async_set_swing_mode(hass, "on", ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with( + "swing-mode-topic", "swing_mode: on", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("swing_mode") == "on" + + # Temperature + await common.async_set_temperature(hass, temperature=47, entity_id=ENTITY_CLIMATE) + mqtt_mock.async_publish.assert_called_once_with( + "temperature-topic", "temp: 47.0", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("temperature") == 47 + + # Temperature Low/High + await common.async_set_temperature( + hass, target_temp_low=20, target_temp_high=23, entity_id=ENTITY_CLIMATE + ) + mqtt_mock.async_publish.assert_any_call( + "temperature-low-topic", "temp_lo: 20.0", 0, False + ) + mqtt_mock.async_publish.assert_any_call( + "temperature-high-topic", "temp_hi: 23.0", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get(ENTITY_CLIMATE) + assert state.attributes.get("target_temp_low") == 20 + assert state.attributes.get("target_temp_high") == 23 + + async def test_min_temp_custom(hass, mqtt_mock): """Test a custom min temp.""" config = copy.deepcopy(DEFAULT_CONFIG) diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index e706bc47418..c8cd80372c6 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -2,6 +2,7 @@ import copy from datetime import datetime import json +from unittest.mock import ANY, patch from homeassistant.components import mqtt from homeassistant.components.mqtt import debug_info @@ -10,7 +11,6 @@ from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNAVAILABLE from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component -from tests.async_mock import ANY, patch from tests.common import async_fire_mqtt_message, mock_registry DEFAULT_CONFIG_DEVICE_INFO_ID = { @@ -171,6 +171,135 @@ async def help_test_default_availability_list_payload( assert state.state != STATE_UNAVAILABLE +async def help_test_default_availability_list_payload_all( + hass, + mqtt_mock, + domain, + config, + no_assumed_state=False, + state_topic=None, + state_message=None, +): + """Test availability by default payload with defined topic. + + This is a test helper for the MqttAvailability mixin. + """ + # Add availability settings to config + config = copy.deepcopy(config) + config[domain]["availability_mode"] = "all" + config[domain]["availability"] = [ + {"topic": "availability-topic1"}, + {"topic": "availability-topic2"}, + ] + assert await async_setup_component( + hass, + domain, + config, + ) + await hass.async_block_till_done() + + state = hass.states.get(f"{domain}.test") + assert state.state == STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, "availability-topic1", "online") + + state = hass.states.get(f"{domain}.test") + assert state.state == STATE_UNAVAILABLE + if no_assumed_state: + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "availability-topic2", "online") + + state = hass.states.get(f"{domain}.test") + assert state.state != STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, "availability-topic2", "offline") + + state = hass.states.get(f"{domain}.test") + assert state.state == STATE_UNAVAILABLE + if no_assumed_state: + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "availability-topic2", "online") + + state = hass.states.get(f"{domain}.test") + assert state.state != STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, "availability-topic1", "offline") + + state = hass.states.get(f"{domain}.test") + assert state.state == STATE_UNAVAILABLE + if no_assumed_state: + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "availability-topic1", "online") + + state = hass.states.get(f"{domain}.test") + assert state.state != STATE_UNAVAILABLE + + +async def help_test_default_availability_list_payload_any( + hass, + mqtt_mock, + domain, + config, + no_assumed_state=False, + state_topic=None, + state_message=None, +): + """Test availability by default payload with defined topic. + + This is a test helper for the MqttAvailability mixin. + """ + # Add availability settings to config + config = copy.deepcopy(config) + config[domain]["availability_mode"] = "any" + config[domain]["availability"] = [ + {"topic": "availability-topic1"}, + {"topic": "availability-topic2"}, + ] + assert await async_setup_component( + hass, + domain, + config, + ) + await hass.async_block_till_done() + + state = hass.states.get(f"{domain}.test") + assert state.state == STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, "availability-topic1", "online") + + state = hass.states.get(f"{domain}.test") + assert state.state != STATE_UNAVAILABLE + if no_assumed_state: + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "availability-topic2", "online") + + state = hass.states.get(f"{domain}.test") + assert state.state != STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, "availability-topic2", "offline") + + state = hass.states.get(f"{domain}.test") + assert state.state != STATE_UNAVAILABLE + if no_assumed_state: + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "availability-topic1", "offline") + + state = hass.states.get(f"{domain}.test") + assert state.state == STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, "availability-topic1", "online") + + state = hass.states.get(f"{domain}.test") + assert state.state != STATE_UNAVAILABLE + if no_assumed_state: + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async def help_test_default_availability_list_single( hass, mqtt_mock, @@ -602,7 +731,7 @@ async def help_test_entity_device_info_with_identifier(hass, mqtt_mock, domain, async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None assert device.identifiers == {("mqtt", "helloworld")} assert device.manufacturer == "Whatever" @@ -650,14 +779,14 @@ async def help_test_entity_device_info_remove(hass, mqtt_mock, domain, config): async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = dev_registry.async_get_device({("mqtt", "helloworld")}, set()) + device = dev_registry.async_get_device({("mqtt", "helloworld")}) assert device is not None assert ent_registry.async_get_entity_id(domain, mqtt.DOMAIN, "veryunique") async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", "") await hass.async_block_till_done() - device = dev_registry.async_get_device({("mqtt", "helloworld")}, set()) + device = dev_registry.async_get_device({("mqtt", "helloworld")}) assert device is None assert not ent_registry.async_get_entity_id(domain, mqtt.DOMAIN, "veryunique") @@ -678,7 +807,7 @@ async def help_test_entity_device_info_update(hass, mqtt_mock, domain, config): async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None assert device.name == "Beer" @@ -687,7 +816,7 @@ async def help_test_entity_device_info_update(hass, mqtt_mock, domain, config): async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None assert device.name == "Milk" @@ -790,7 +919,7 @@ async def help_test_entity_debug_info(hass, mqtt_mock, domain, config): async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None debug_info_data = await debug_info.info_for_device(hass, device.id) @@ -823,7 +952,7 @@ async def help_test_entity_debug_info_max_messages(hass, mqtt_mock, domain, conf async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None debug_info_data = await debug_info.info_for_device(hass, device.id) @@ -885,7 +1014,7 @@ async def help_test_entity_debug_info_message( async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None debug_info_data = await debug_info.info_for_device(hass, device.id) @@ -931,7 +1060,7 @@ async def help_test_entity_debug_info_remove(hass, mqtt_mock, domain, config): async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None debug_info_data = await debug_info.info_for_device(hass, device.id) @@ -975,7 +1104,7 @@ async def help_test_entity_debug_info_update_entity_id(hass, mqtt_mock, domain, async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = dev_registry.async_get_device({("mqtt", "helloworld")}, set()) + device = dev_registry.async_get_device({("mqtt", "helloworld")}) assert device is not None debug_info_data = await debug_info.info_for_device(hass, device.id) diff --git a/tests/components/mqtt/test_config_flow.py b/tests/components/mqtt/test_config_flow.py index 41dc4745528..b41a446a8c0 100644 --- a/tests/components/mqtt/test_config_flow.py +++ b/tests/components/mqtt/test_config_flow.py @@ -1,5 +1,7 @@ """Test config flow.""" +from unittest.mock import patch + import pytest import voluptuous as vol @@ -7,7 +9,6 @@ from homeassistant import data_entry_flow from homeassistant.components import mqtt from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 629d9674b22..019f0e19911 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -1,4 +1,6 @@ """The tests for the MQTT cover platform.""" +from unittest.mock import patch + import pytest from homeassistant.components import cover @@ -53,7 +55,6 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.async_mock import patch from tests.common import async_fire_mqtt_message DEFAULT_CONFIG = { diff --git a/tests/components/mqtt/test_device_tracker.py b/tests/components/mqtt/test_device_tracker.py index 32f826422a8..c85fcef7dc4 100644 --- a/tests/components/mqtt/test_device_tracker.py +++ b/tests/components/mqtt/test_device_tracker.py @@ -1,11 +1,12 @@ """The tests for the MQTT device tracker platform.""" +from unittest.mock import patch + import pytest from homeassistant.components.device_tracker.const import DOMAIN, SOURCE_TYPE_BLUETOOTH from homeassistant.const import CONF_PLATFORM, STATE_HOME, STATE_NOT_HOME from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import async_fire_mqtt_message diff --git a/tests/components/mqtt/test_device_tracker_discovery.py b/tests/components/mqtt/test_device_tracker_discovery.py index 4ee6986e599..2c445ee0fa5 100644 --- a/tests/components/mqtt/test_device_tracker_discovery.py +++ b/tests/components/mqtt/test_device_tracker_discovery.py @@ -184,7 +184,7 @@ async def test_cleanup_device_tracker(hass, device_reg, entity_reg, mqtt_mock): await hass.async_block_till_done() # Verify device and registry entries are created - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert device_entry is not None entity_entry = entity_reg.async_get("device_tracker.mqtt_unique") assert entity_entry is not None @@ -196,7 +196,7 @@ async def test_cleanup_device_tracker(hass, device_reg, entity_reg, mqtt_mock): await hass.async_block_till_done() # Verify device and registry entries are cleared - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert device_entry is None entity_entry = entity_reg.async_get("device_tracker.mqtt_unique") assert entity_entry is None diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index a46406c330f..f200de6a274 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -50,7 +50,7 @@ async def test_get_triggers(hass, device_reg, entity_reg, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data1) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) expected_triggers = [ { "platform": "device", @@ -76,7 +76,7 @@ async def test_get_unknown_triggers(hass, device_reg, entity_reg, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data1) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert await async_setup_component( hass, @@ -116,7 +116,7 @@ async def test_get_non_existing_triggers(hass, device_reg, entity_reg, mqtt_mock async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data1) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) triggers = await async_get_device_automations(hass, "trigger", device_entry.id) assert_lists_same(triggers, []) @@ -135,7 +135,7 @@ async def test_discover_bad_triggers(hass, device_reg, entity_reg, mqtt_mock): ) async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data0) await hass.async_block_till_done() - assert device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) is None + assert device_reg.async_get_device({("mqtt", "0AFFD2")}) is None # Test sending correct data data1 = ( @@ -149,7 +149,7 @@ async def test_discover_bad_triggers(hass, device_reg, entity_reg, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data1) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) expected_triggers = [ { "platform": "device", @@ -185,7 +185,7 @@ async def test_update_remove_triggers(hass, device_reg, entity_reg, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data1) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) expected_triggers1 = [ { "platform": "device", @@ -213,7 +213,7 @@ async def test_update_remove_triggers(hass, device_reg, entity_reg, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", "") await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert device_entry is None @@ -238,7 +238,7 @@ async def test_if_fires_on_mqtt_message(hass, device_reg, calls, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) async_fire_mqtt_message(hass, "homeassistant/device_automation/bla2/config", data2) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert await async_setup_component( hass, @@ -317,7 +317,7 @@ async def test_if_fires_on_mqtt_message_late_discover( ) async_fire_mqtt_message(hass, "homeassistant/sensor/bla0/config", data0) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert await async_setup_component( hass, @@ -393,7 +393,7 @@ async def test_if_fires_on_mqtt_message_after_update( ) async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert await async_setup_component( hass, @@ -459,7 +459,7 @@ async def test_no_resubscribe_same_topic(hass, device_reg, mqtt_mock): ) async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert await async_setup_component( hass, @@ -503,7 +503,7 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt( ) async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert await async_setup_component( hass, @@ -563,7 +563,7 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( ) async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert await async_setup_component( hass, @@ -614,7 +614,7 @@ async def test_attach_remove(hass, device_reg, mqtt_mock): ) async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) calls = [] @@ -668,7 +668,7 @@ async def test_attach_remove_late(hass, device_reg, mqtt_mock): ) async_fire_mqtt_message(hass, "homeassistant/sensor/bla0/config", data0) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) calls = [] @@ -725,7 +725,7 @@ async def test_attach_remove_late2(hass, device_reg, mqtt_mock): ) async_fire_mqtt_message(hass, "homeassistant/sensor/bla0/config", data0) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) calls = [] @@ -812,7 +812,7 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None assert device.identifiers == {("mqtt", "helloworld")} assert device.manufacturer == "Whatever" @@ -844,7 +844,7 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None assert device.name == "Beer" @@ -853,7 +853,7 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None assert device.name == "Milk" @@ -873,7 +873,7 @@ async def test_cleanup_trigger(hass, device_reg, entity_reg, mqtt_mock): await hass.async_block_till_done() # Verify device registry entry is created - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None triggers = await async_get_device_automations(hass, "trigger", device_entry.id) @@ -884,7 +884,7 @@ async def test_cleanup_trigger(hass, device_reg, entity_reg, mqtt_mock): await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is None # Verify retained discovery topic has been cleared @@ -908,7 +908,7 @@ async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock): await hass.async_block_till_done() # Verify device registry entry is created - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None triggers = await async_get_device_automations(hass, "trigger", device_entry.id) @@ -918,7 +918,7 @@ async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock): await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is None @@ -948,7 +948,7 @@ async def test_cleanup_device_several_triggers(hass, device_reg, entity_reg, mqt await hass.async_block_till_done() # Verify device registry entry is created - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None triggers = await async_get_device_automations(hass, "trigger", device_entry.id) @@ -960,7 +960,7 @@ async def test_cleanup_device_several_triggers(hass, device_reg, entity_reg, mqt await hass.async_block_till_done() # Verify device registry entry is not cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None triggers = await async_get_device_automations(hass, "trigger", device_entry.id) @@ -971,7 +971,7 @@ async def test_cleanup_device_several_triggers(hass, device_reg, entity_reg, mqt await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is None @@ -1003,7 +1003,7 @@ async def test_cleanup_device_with_entity1(hass, device_reg, entity_reg, mqtt_mo await hass.async_block_till_done() # Verify device registry entry is created - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None triggers = await async_get_device_automations(hass, "trigger", device_entry.id) @@ -1013,7 +1013,7 @@ async def test_cleanup_device_with_entity1(hass, device_reg, entity_reg, mqtt_mo await hass.async_block_till_done() # Verify device registry entry is not cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None triggers = await async_get_device_automations(hass, "trigger", device_entry.id) @@ -1023,7 +1023,7 @@ async def test_cleanup_device_with_entity1(hass, device_reg, entity_reg, mqtt_mo await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is None @@ -1055,7 +1055,7 @@ async def test_cleanup_device_with_entity2(hass, device_reg, entity_reg, mqtt_mo await hass.async_block_till_done() # Verify device registry entry is created - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None triggers = await async_get_device_automations(hass, "trigger", device_entry.id) @@ -1065,7 +1065,7 @@ async def test_cleanup_device_with_entity2(hass, device_reg, entity_reg, mqtt_mo await hass.async_block_till_done() # Verify device registry entry is not cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None triggers = await async_get_device_automations(hass, "trigger", device_entry.id) @@ -1075,7 +1075,7 @@ async def test_cleanup_device_with_entity2(hass, device_reg, entity_reg, mqtt_mo await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is None diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index e1365418483..c9b0879d490 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -1,6 +1,7 @@ """The tests for the MQTT discovery.""" from pathlib import Path import re +from unittest.mock import AsyncMock, patch import pytest @@ -11,9 +12,9 @@ from homeassistant.components.mqtt.abbreviations import ( DEVICE_ABBREVIATIONS, ) from homeassistant.components.mqtt.discovery import ALREADY_DISCOVERED, async_start -from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.const import EVENT_STATE_CHANGED, STATE_OFF, STATE_ON +import homeassistant.core as ha -from tests.async_mock import AsyncMock, patch from tests.common import ( async_fire_mqtt_message, mock_device_registry, @@ -45,10 +46,13 @@ async def test_subscribing_config_topic(hass, mqtt_mock): discovery_topic = "homeassistant" await async_start(hass, discovery_topic, entry) - assert mqtt_mock.async_subscribe.called - call_args = mqtt_mock.async_subscribe.mock_calls[0][1] - assert call_args[0] == discovery_topic + "/#" - assert call_args[2] == 0 + call_args1 = mqtt_mock.async_subscribe.mock_calls[0][1] + assert call_args1[2] == 0 + call_args2 = mqtt_mock.async_subscribe.mock_calls[1][1] + assert call_args2[2] == 0 + topics = [call_args1[0], call_args2[0]] + assert discovery_topic + "/+/+/config" in topics + assert discovery_topic + "/+/+/+/config" in topics async def test_invalid_topic(hass, mqtt_mock): @@ -252,6 +256,121 @@ async def test_rediscover(hass, mqtt_mock, caplog): assert state is not None +async def test_rapid_rediscover(hass, mqtt_mock, caplog): + """Test immediate rediscover of removed component.""" + + events = [] + + @ha.callback + def callback(event): + """Verify event got called.""" + events.append(event) + + hass.bus.async_listen(EVENT_STATE_CHANGED, callback) + + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', + ) + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.beer") + assert state is not None + assert len(events) == 1 + + # Removal immediately followed by rediscover + async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla/config", "") + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', + ) + async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla/config", "") + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Milk", "state_topic": "test-topic" }', + ) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids("binary_sensor")) == 1 + state = hass.states.get("binary_sensor.milk") + assert state is not None + + assert len(events) == 5 + # Remove the entity + assert events[1].data["entity_id"] == "binary_sensor.beer" + assert events[1].data["new_state"] is None + # Add the entity + assert events[2].data["entity_id"] == "binary_sensor.beer" + assert events[2].data["old_state"] is None + # Remove the entity + assert events[3].data["entity_id"] == "binary_sensor.beer" + assert events[3].data["new_state"] is None + # Add the entity + assert events[4].data["entity_id"] == "binary_sensor.milk" + assert events[4].data["old_state"] is None + + +async def test_rapid_rediscover_unique(hass, mqtt_mock, caplog): + """Test immediate rediscover of removed component.""" + + events = [] + + @ha.callback + def callback(event): + """Verify event got called.""" + events.append(event) + + hass.bus.async_listen(EVENT_STATE_CHANGED, callback) + + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla2/config", + '{ "name": "Ale", "state_topic": "test-topic", "unique_id": "very_unique" }', + ) + await hass.async_block_till_done() + state = hass.states.get("binary_sensor.ale") + assert state is not None + assert len(events) == 1 + + # Duplicate unique_id, immediately followed by correct unique_id + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Beer", "state_topic": "test-topic", "unique_id": "very_unique" }', + ) + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Beer", "state_topic": "test-topic", "unique_id": "even_uniquer" }', + ) + async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla/config", "") + async_fire_mqtt_message( + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Milk", "state_topic": "test-topic", "unique_id": "even_uniquer" }', + ) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids("binary_sensor")) == 2 + state = hass.states.get("binary_sensor.ale") + assert state is not None + state = hass.states.get("binary_sensor.milk") + assert state is not None + + assert len(events) == 4 + # Add the entity + assert events[1].data["entity_id"] == "binary_sensor.beer" + assert events[1].data["old_state"] is None + # Remove the entity + assert events[2].data["entity_id"] == "binary_sensor.beer" + assert events[2].data["new_state"] is None + # Add the entity + assert events[3].data["entity_id"] == "binary_sensor.milk" + assert events[3].data["old_state"] is None + + async def test_duplicate_removal(hass, mqtt_mock, caplog): """Test for a non duplicate component.""" async_fire_mqtt_message( @@ -282,7 +401,7 @@ async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock): await hass.async_block_till_done() # Verify device and registry entries are created - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert device_entry is not None entity_entry = entity_reg.async_get("sensor.mqtt_sensor") assert entity_entry is not None @@ -294,7 +413,7 @@ async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock): await hass.async_block_till_done() # Verify device and registry entries are cleared - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert device_entry is None entity_entry = entity_reg.async_get("sensor.mqtt_sensor") assert entity_entry is None diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index e1801c5c15a..045b8fdaf0e 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -1,4 +1,6 @@ """Test MQTT fans.""" +from unittest.mock import patch + import pytest from homeassistant.components import fan @@ -34,7 +36,6 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.async_mock import patch from tests.common import async_fire_mqtt_message from tests.components.fan import common @@ -434,14 +435,8 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): assert state.state is STATE_OFF assert state.attributes.get(ATTR_ASSUMED_STATE) - await common.async_set_speed(hass, "fan.test", "cUsToM") - mqtt_mock.async_publish.assert_called_once_with( - "speed-command-topic", "cUsToM", 0, False - ) - mqtt_mock.async_publish.reset_mock() - state = hass.states.get("fan.test") - assert state.state is STATE_OFF - assert state.attributes.get(ATTR_ASSUMED_STATE) + with pytest.raises(ValueError): + await common.async_set_speed(hass, "fan.test", "cUsToM") async def test_attributes(hass, mqtt_mock): @@ -521,12 +516,8 @@ async def test_attributes(hass, mqtt_mock): assert state.attributes.get(fan.ATTR_SPEED) == "off" assert state.attributes.get(fan.ATTR_OSCILLATING) is False - await common.async_set_speed(hass, "fan.test", "cUsToM") - state = hass.states.get("fan.test") - assert state.state is STATE_OFF - assert state.attributes.get(ATTR_ASSUMED_STATE) - assert state.attributes.get(fan.ATTR_SPEED) == "cUsToM" - assert state.attributes.get(fan.ATTR_OSCILLATING) is False + with pytest.raises(ValueError): + await common.async_set_speed(hass, "fan.test", "cUsToM") async def test_custom_speed_list(hass, mqtt_mock): diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 82a88de918b..2907a0e4cfc 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -3,12 +3,14 @@ import asyncio from datetime import datetime, timedelta import json import ssl +from unittest.mock import AsyncMock, MagicMock, call, mock_open, patch import pytest import voluptuous as vol from homeassistant.components import mqtt, websocket_api from homeassistant.components.mqtt import debug_info +from homeassistant.components.mqtt.mixins import MQTT_ENTITY_DEVICE_INFO_SCHEMA from homeassistant.const import ( ATTR_DOMAIN, ATTR_SERVICE, @@ -21,7 +23,6 @@ from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from tests.async_mock import AsyncMock, MagicMock, call, mock_open, patch from tests.common import ( MockConfigEntry, async_fire_mqtt_message, @@ -241,12 +242,12 @@ def test_validate_publish_topic(): def test_entity_device_info_schema(): """Test MQTT entity device info validation.""" # just identifier - mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA({"identifiers": ["abcd"]}) - mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA({"identifiers": "abcd"}) + MQTT_ENTITY_DEVICE_INFO_SCHEMA({"identifiers": ["abcd"]}) + MQTT_ENTITY_DEVICE_INFO_SCHEMA({"identifiers": "abcd"}) # just connection - mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA({"connections": [["mac", "02:5b:26:a8:dc:12"]]}) + MQTT_ENTITY_DEVICE_INFO_SCHEMA({"connections": [["mac", "02:5b:26:a8:dc:12"]]}) # full device info - mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA( + MQTT_ENTITY_DEVICE_INFO_SCHEMA( { "identifiers": ["helloworld", "hello"], "connections": [["mac", "02:5b:26:a8:dc:12"], ["zigbee", "zigbee_id"]], @@ -257,7 +258,7 @@ def test_entity_device_info_schema(): } ) # full device info with via_device - mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA( + MQTT_ENTITY_DEVICE_INFO_SCHEMA( { "identifiers": ["helloworld", "hello"], "connections": [["mac", "02:5b:26:a8:dc:12"], ["zigbee", "zigbee_id"]], @@ -270,7 +271,7 @@ def test_entity_device_info_schema(): ) # no identifiers with pytest.raises(vol.Invalid): - mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA( + MQTT_ENTITY_DEVICE_INFO_SCHEMA( { "manufacturer": "Whatever", "name": "Beer", @@ -280,7 +281,7 @@ def test_entity_device_info_schema(): ) # empty identifiers with pytest.raises(vol.Invalid): - mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA( + MQTT_ENTITY_DEVICE_INFO_SCHEMA( {"identifiers": [], "connections": [], "name": "Beer"} ) @@ -970,7 +971,7 @@ async def test_mqtt_ws_remove_discovered_device( await hass.async_block_till_done() # Verify device entry is created - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert device_entry is not None client = await hass_ws_client(hass) @@ -981,7 +982,7 @@ async def test_mqtt_ws_remove_discovered_device( assert response["success"] # Verify device entry is cleared - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert device_entry is None @@ -998,7 +999,7 @@ async def test_mqtt_ws_remove_discovered_device_twice( async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert device_entry is not None client = await hass_ws_client(hass) @@ -1030,7 +1031,7 @@ async def test_mqtt_ws_remove_discovered_device_same_topic( async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert device_entry is not None client = await hass_ws_client(hass) @@ -1086,7 +1087,7 @@ async def test_mqtt_ws_get_device_debug_info( await hass.async_block_till_done() # Verify device entry is created - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) assert device_entry is not None client = await hass_ws_client(hass) @@ -1168,7 +1169,7 @@ async def test_debug_info_multiple_devices(hass, mqtt_mock): for d in devices: domain = d["domain"] id = d["config"]["device"]["identifiers"][0] - device = registry.async_get_device({("mqtt", id)}, set()) + device = registry.async_get_device({("mqtt", id)}) assert device is not None debug_info_data = await debug_info.info_for_device(hass, device.id) @@ -1246,7 +1247,7 @@ async def test_debug_info_multiple_entities_triggers(hass, mqtt_mock): await hass.async_block_till_done() device_id = config[0]["config"]["device"]["identifiers"][0] - device = registry.async_get_device({("mqtt", device_id)}, set()) + device = registry.async_get_device({("mqtt", device_id)}) assert device is not None debug_info_data = await debug_info.info_for_device(hass, device.id) assert len(debug_info_data["entities"]) == 2 @@ -1316,7 +1317,7 @@ async def test_debug_info_wildcard(hass, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None debug_info_data = await debug_info.info_for_device(hass, device.id) @@ -1362,7 +1363,7 @@ async def test_debug_info_filter_same(hass, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None debug_info_data = await debug_info.info_for_device(hass, device.id) @@ -1421,7 +1422,7 @@ async def test_debug_info_same_topic(hass, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None debug_info_data = await debug_info.info_for_device(hass, device.id) @@ -1472,7 +1473,7 @@ async def test_debug_info_qos_retain(hass, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None debug_info_data = await debug_info.info_for_device(hass, device.id) diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index aacea4e345e..db25e66c2c2 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -1,6 +1,7 @@ """The tests for the Legacy Mqtt vacuum platform.""" from copy import deepcopy import json +from unittest.mock import patch import pytest @@ -46,7 +47,6 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.async_mock import patch from tests.common import async_fire_mqtt_message from tests.components.vacuum import common diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 9e044380555..933f49ff823 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -155,6 +155,7 @@ light: """ import json from os import path +from unittest.mock import call, patch import pytest @@ -188,7 +189,6 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.async_mock import call, patch from tests.common import assert_setup_component, async_fire_mqtt_message from tests.components.light import common diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index e41575968da..022df109f38 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -88,6 +88,7 @@ light: brightness_scale: 99 """ import json +from unittest.mock import call, patch import pytest @@ -125,7 +126,6 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.async_mock import call, patch from tests.common import async_fire_mqtt_message from tests.components.light import common @@ -295,11 +295,21 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): light_state = hass.states.get("light.test") assert light_state.attributes.get("hs_color") == (180.0, 50.0) + async_fire_mqtt_message(hass, "test_light_rgb", '{"state":"ON", "color":null}') + + light_state = hass.states.get("light.test") + assert "hs_color" not in light_state.attributes + async_fire_mqtt_message(hass, "test_light_rgb", '{"state":"ON", "color_temp":155}') light_state = hass.states.get("light.test") assert light_state.attributes.get("color_temp") == 155 + async_fire_mqtt_message(hass, "test_light_rgb", '{"state":"ON", "color_temp":null}') + + light_state = hass.states.get("light.test") + assert "color_temp" not in light_state.attributes + async_fire_mqtt_message( hass, "test_light_rgb", '{"state":"ON", "effect":"colorloop"}' ) @@ -1004,6 +1014,18 @@ async def test_invalid_values(hass, mqtt_mock): assert state.attributes.get("white_value") == 255 assert state.attributes.get("color_temp") == 100 + # Empty color value + async_fire_mqtt_message( + hass, + "test_light_rgb", + '{"state":"ON",' '"color":{}}', + ) + + # Color should not have changed + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == (255, 255, 255) + # Bad HS color values async_fire_mqtt_message( hass, diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 6767375a50e..733a39ce252 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -26,6 +26,8 @@ If your light doesn't support white value feature, omit `white_value_template`. If your light doesn't support RGB feature, omit `(red|green|blue)_template`. """ +from unittest.mock import patch + import pytest from homeassistant.components import light @@ -62,7 +64,6 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.async_mock import patch from tests.common import assert_setup_component, async_fire_mqtt_message from tests.components.light import common diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index cd37543d94e..754f60f49b2 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -1,4 +1,6 @@ """The tests for the MQTT lock platform.""" +from unittest.mock import patch + import pytest from homeassistant.components.lock import ( @@ -35,7 +37,6 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.async_mock import patch from tests.common import async_fire_mqtt_message DEFAULT_CONFIG = { diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py new file mode 100644 index 00000000000..ac5285e9855 --- /dev/null +++ b/tests/components/mqtt/test_number.py @@ -0,0 +1,359 @@ +"""The tests for mqtt number component.""" +import json +from unittest.mock import patch + +import pytest + +from homeassistant.components import number +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ASSUMED_STATE, ATTR_ENTITY_ID +import homeassistant.core as ha +from homeassistant.setup import async_setup_component + +from .test_common import ( + help_test_availability_when_connection_lost, + help_test_availability_without_topic, + help_test_custom_availability_payload, + help_test_default_availability_payload, + help_test_discovery_broken, + help_test_discovery_removal, + help_test_discovery_update, + help_test_discovery_update_attr, + help_test_discovery_update_unchanged, + help_test_entity_debug_info_message, + help_test_entity_device_info_remove, + help_test_entity_device_info_update, + help_test_entity_device_info_with_connection, + help_test_entity_device_info_with_identifier, + help_test_entity_id_update_discovery_update, + help_test_entity_id_update_subscriptions, + help_test_setting_attribute_via_mqtt_json_message, + help_test_setting_attribute_with_template, + help_test_unique_id, + help_test_update_with_json_attrs_bad_JSON, + help_test_update_with_json_attrs_not_dict, +) + +from tests.common import async_fire_mqtt_message + +DEFAULT_CONFIG = { + number.DOMAIN: {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} +} + + +async def test_run_number_setup(hass, mqtt_mock): + """Test that it fetches the given payload.""" + topic = "test/number" + await async_setup_component( + hass, + "number", + { + "number": { + "platform": "mqtt", + "state_topic": topic, + "command_topic": topic, + "name": "Test Number", + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, topic, "10") + + await hass.async_block_till_done() + + state = hass.states.get("number.test_number") + assert state.state == "10" + + async_fire_mqtt_message(hass, topic, "20.5") + + await hass.async_block_till_done() + + state = hass.states.get("number.test_number") + assert state.state == "20.5" + + +async def test_run_number_service_optimistic(hass, mqtt_mock): + """Test that set_value service works in optimistic mode.""" + topic = "test/number" + + fake_state = ha.State("switch.test", "3") + + with patch( + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", + return_value=fake_state, + ): + assert await async_setup_component( + hass, + number.DOMAIN, + { + "number": { + "platform": "mqtt", + "command_topic": topic, + "name": "Test Number", + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("number.test_number") + assert state.state == "3" + assert state.attributes.get(ATTR_ASSUMED_STATE) + + # Integer + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: "number.test_number", ATTR_VALUE: 30}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with(topic, "30", 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("number.test_number") + assert state.state == "30" + + # Float with no decimal -> integer + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: "number.test_number", ATTR_VALUE: 42.0}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with(topic, "42", 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("number.test_number") + assert state.state == "42" + + # Float with decimal -> float + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: "number.test_number", ATTR_VALUE: 42.1}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with(topic, "42.1", 0, False) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("number.test_number") + assert state.state == "42.1" + + +async def test_run_number_service(hass, mqtt_mock): + """Test that set_value service works in non optimistic mode.""" + cmd_topic = "test/number/set" + state_topic = "test/number" + + assert await async_setup_component( + hass, + number.DOMAIN, + { + "number": { + "platform": "mqtt", + "command_topic": cmd_topic, + "state_topic": state_topic, + "name": "Test Number", + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, state_topic, "32") + state = hass.states.get("number.test_number") + assert state.state == "32" + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + {ATTR_ENTITY_ID: "number.test_number", ATTR_VALUE: 30}, + blocking=True, + ) + mqtt_mock.async_publish.assert_called_once_with(cmd_topic, "30", 0, False) + state = hass.states.get("number.test_number") + assert state.state == "32" + + +async def test_availability_when_connection_lost(hass, mqtt_mock): + """Test availability after MQTT disconnection.""" + await help_test_availability_when_connection_lost( + hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_availability_without_topic(hass, mqtt_mock): + """Test availability without defined availability topic.""" + await help_test_availability_without_topic( + hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_default_availability_payload(hass, mqtt_mock): + """Test availability by default payload with defined topic.""" + await help_test_default_availability_payload( + hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_custom_availability_payload(hass, mqtt_mock): + """Test availability by custom payload with defined topic.""" + await help_test_custom_availability_payload( + hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_attribute_via_mqtt_json_message( + hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_setting_attribute_with_template(hass, mqtt_mock): + """Test the setting of attribute via MQTT with JSON payload.""" + await help_test_setting_attribute_with_template( + hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_update_with_json_attrs_not_dict(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + await help_test_update_with_json_attrs_not_dict( + hass, mqtt_mock, caplog, number.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_update_with_json_attrs_bad_JSON(hass, mqtt_mock, caplog): + """Test attributes get extracted from a JSON result.""" + await help_test_update_with_json_attrs_bad_JSON( + hass, mqtt_mock, caplog, number.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_discovery_update_attr(hass, mqtt_mock, caplog): + """Test update of discovered MQTTAttributes.""" + await help_test_discovery_update_attr( + hass, mqtt_mock, caplog, number.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_unique_id(hass, mqtt_mock): + """Test unique id option only creates one number per unique_id.""" + config = { + number.DOMAIN: [ + { + "platform": "mqtt", + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "platform": "mqtt", + "name": "Test 2", + "state_topic": "test-topic", + "command_topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } + await help_test_unique_id(hass, mqtt_mock, number.DOMAIN, config) + + +async def test_discovery_removal_number(hass, mqtt_mock, caplog): + """Test removal of discovered number.""" + data = json.dumps(DEFAULT_CONFIG[number.DOMAIN]) + await help_test_discovery_removal(hass, mqtt_mock, caplog, number.DOMAIN, data) + + +async def test_discovery_update_number(hass, mqtt_mock, caplog): + """Test update of discovered number.""" + data1 = ( + '{ "name": "Beer", "state_topic": "test-topic", "command_topic": "test-topic"}' + ) + data2 = ( + '{ "name": "Milk", "state_topic": "test-topic", "command_topic": "test-topic"}' + ) + + await help_test_discovery_update( + hass, mqtt_mock, caplog, number.DOMAIN, data1, data2 + ) + + +async def test_discovery_update_unchanged_number(hass, mqtt_mock, caplog): + """Test update of discovered number.""" + data1 = ( + '{ "name": "Beer", "state_topic": "test-topic", "command_topic": "test-topic"}' + ) + with patch( + "homeassistant.components.mqtt.number.MqttNumber.discovery_update" + ) as discovery_update: + await help_test_discovery_update_unchanged( + hass, mqtt_mock, caplog, number.DOMAIN, data1, discovery_update + ) + + +@pytest.mark.no_fail_on_log_exception +async def test_discovery_broken(hass, mqtt_mock, caplog): + """Test handling of bad discovery message.""" + data1 = '{ "name": "Beer" }' + data2 = ( + '{ "name": "Milk", "state_topic": "test-topic", "command_topic": "test-topic"}' + ) + + await help_test_discovery_broken( + hass, mqtt_mock, caplog, number.DOMAIN, data1, data2 + ) + + +async def test_entity_device_info_with_connection(hass, mqtt_mock): + """Test MQTT number device registry integration.""" + await help_test_entity_device_info_with_connection( + hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_with_identifier(hass, mqtt_mock): + """Test MQTT number device registry integration.""" + await help_test_entity_device_info_with_identifier( + hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_update(hass, mqtt_mock): + """Test device registry update.""" + await help_test_entity_device_info_update( + hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_device_info_remove(hass, mqtt_mock): + """Test device registry remove.""" + await help_test_entity_device_info_remove( + hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_id_update_subscriptions(hass, mqtt_mock): + """Test MQTT subscriptions are managed when entity_id is updated.""" + await help_test_entity_id_update_subscriptions( + hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_id_update_discovery_update(hass, mqtt_mock): + """Test MQTT discovery update when entity_id is updated.""" + await help_test_entity_id_update_discovery_update( + hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_debug_info_message(hass, mqtt_mock): + """Test MQTT debug info.""" + await help_test_entity_debug_info_message( + hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG, payload=b"1" + ) diff --git a/tests/components/mqtt/test_scene.py b/tests/components/mqtt/test_scene.py index 0e3341bd15f..9a233e19fd8 100644 --- a/tests/components/mqtt/test_scene.py +++ b/tests/components/mqtt/test_scene.py @@ -1,6 +1,7 @@ """The tests for the MQTT scene platform.""" import copy import json +from unittest.mock import patch import pytest @@ -21,8 +22,6 @@ from .test_common import ( help_test_unique_id, ) -from tests.async_mock import patch - DEFAULT_CONFIG = { scene.DOMAIN: { "platform": "mqtt", diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 7e92579753b..a2c2605d6ab 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -2,6 +2,7 @@ import copy from datetime import datetime, timedelta import json +from unittest.mock import patch import pytest @@ -16,6 +17,8 @@ from .test_common import ( help_test_availability_without_topic, help_test_custom_availability_payload, help_test_default_availability_list_payload, + help_test_default_availability_list_payload_all, + help_test_default_availability_list_payload_any, help_test_default_availability_list_single, help_test_default_availability_payload, help_test_discovery_broken, @@ -42,7 +45,6 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.async_mock import patch from tests.common import async_fire_mqtt_message, async_fire_time_changed DEFAULT_CONFIG = { @@ -297,6 +299,20 @@ async def test_default_availability_list_payload(hass, mqtt_mock): ) +async def test_default_availability_list_payload_all(hass, mqtt_mock): + """Test availability by default payload with defined topic.""" + await help_test_default_availability_list_payload_all( + hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_default_availability_list_payload_any(hass, mqtt_mock): + """Test availability by default payload with defined topic.""" + await help_test_default_availability_list_payload_any( + hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + ) + + async def test_default_availability_list_single(hass, mqtt_mock, caplog): """Test availability list and availability_topic are mutually exclusive.""" await help_test_default_availability_list_single( @@ -579,7 +595,7 @@ async def test_entity_device_info_with_hub(hass, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None assert device.via_device_id == hub.id diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index fe410821395..e18b0b05835 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -1,6 +1,7 @@ """The tests for the State vacuum Mqtt platform.""" from copy import deepcopy import json +from unittest.mock import patch import pytest @@ -56,7 +57,6 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.async_mock import patch from tests.common import async_fire_mqtt_message from tests.components.vacuum import common diff --git a/tests/components/mqtt/test_subscription.py b/tests/components/mqtt/test_subscription.py index f1ed26e89cc..36d8946be0b 100644 --- a/tests/components/mqtt/test_subscription.py +++ b/tests/components/mqtt/test_subscription.py @@ -1,11 +1,12 @@ """The tests for the MQTT subscription component.""" +from unittest.mock import ANY + from homeassistant.components.mqtt.subscription import ( async_subscribe_topics, async_unsubscribe_topics, ) from homeassistant.core import callback -from tests.async_mock import ANY from tests.common import async_fire_mqtt_message diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index 4d9c6dc2c77..607e4468a5f 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -1,6 +1,7 @@ """The tests for the MQTT switch platform.""" import copy import json +from unittest.mock import patch import pytest @@ -33,7 +34,6 @@ from .test_common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.async_mock import patch from tests.common import async_fire_mqtt_message from tests.components.switch import common diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index bed05ac6384..67964a36e1a 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -1,10 +1,10 @@ """The tests for MQTT tag scanner.""" import copy import json +from unittest.mock import ANY, patch import pytest -from tests.async_mock import ANY, patch from tests.common import ( async_fire_mqtt_message, async_get_device_automations, @@ -64,13 +64,13 @@ async def test_discover_bad_tag(hass, device_reg, entity_reg, mqtt_mock, tag_moc data0 = '{ "device":{"identifiers":["0AFFD2"]}, "topics": "foobar/tag_scanned" }' async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", data0) await hass.async_block_till_done() - assert device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) is None + assert device_reg.async_get_device({("mqtt", "0AFFD2")}) is None # Test sending correct data async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", json.dumps(config1)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) # Fake tag scan. async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN) await hass.async_block_till_done() @@ -85,7 +85,7 @@ async def test_if_fires_on_mqtt_message_with_device( async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) # Fake tag scan. async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN) @@ -116,7 +116,7 @@ async def test_if_fires_on_mqtt_message_with_template( async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) # Fake tag scan. async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN_JSON) @@ -147,7 +147,7 @@ async def test_if_fires_on_mqtt_message_after_update_with_device( async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config1)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) # Fake tag scan. async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN) @@ -235,7 +235,7 @@ async def test_if_fires_on_mqtt_message_after_update_with_template( async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config1)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) # Fake tag scan. async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN_JSON) @@ -275,7 +275,7 @@ async def test_no_resubscribe_same_topic(hass, device_reg, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) await hass.async_block_till_done() - assert device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + assert device_reg.async_get_device({("mqtt", "0AFFD2")}) call_count = mqtt_mock.async_subscribe.call_count async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) @@ -291,7 +291,7 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt_with_device( async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) # Fake tag scan. async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN) @@ -359,7 +359,7 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}) # Fake tag scan. async_fire_mqtt_message(hass, "foobar/tag_scanned", DEFAULT_TAG_SCAN) @@ -423,7 +423,7 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None assert device.identifiers == {("mqtt", "helloworld")} assert device.manufacturer == "Whatever" @@ -452,7 +452,7 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None assert device.name == "Beer" @@ -461,7 +461,7 @@ async def test_entity_device_info_update(hass, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device({("mqtt", "helloworld")}, set()) + device = registry.async_get_device({("mqtt", "helloworld")}) assert device is not None assert device.name == "Milk" @@ -478,7 +478,7 @@ async def test_cleanup_tag(hass, device_reg, entity_reg, mqtt_mock): await hass.async_block_till_done() # Verify device registry entry is created - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None device_reg.async_remove_device(device_entry.id) @@ -486,7 +486,7 @@ async def test_cleanup_tag(hass, device_reg, entity_reg, mqtt_mock): await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is None # Verify retained discovery topic has been cleared @@ -507,14 +507,14 @@ async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock): await hass.async_block_till_done() # Verify device registry entry is created - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", "") await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is None @@ -538,14 +538,14 @@ async def test_cleanup_device_several_tags( await hass.async_block_till_done() # Verify device registry entry is created - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", "") await hass.async_block_till_done() # Verify device registry entry is not cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None # Fake tag scan. @@ -558,7 +558,7 @@ async def test_cleanup_device_several_tags( await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is None @@ -600,7 +600,7 @@ async def test_cleanup_device_with_entity_and_trigger_1( await hass.async_block_till_done() # Verify device registry entry is created - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None triggers = await async_get_device_automations(hass, "trigger", device_entry.id) @@ -610,7 +610,7 @@ async def test_cleanup_device_with_entity_and_trigger_1( await hass.async_block_till_done() # Verify device registry entry is not cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None async_fire_mqtt_message(hass, "homeassistant/device_automation/bla2/config", "") @@ -620,7 +620,7 @@ async def test_cleanup_device_with_entity_and_trigger_1( await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is None @@ -660,7 +660,7 @@ async def test_cleanup_device_with_entity2(hass, device_reg, entity_reg, mqtt_mo await hass.async_block_till_done() # Verify device registry entry is created - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None triggers = await async_get_device_automations(hass, "trigger", device_entry.id) @@ -673,12 +673,12 @@ async def test_cleanup_device_with_entity2(hass, device_reg, entity_reg, mqtt_mo await hass.async_block_till_done() # Verify device registry entry is not cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is not None async_fire_mqtt_message(hass, "homeassistant/tag/bla1/config", "") await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}) assert device_entry is None diff --git a/tests/components/mqtt/test_trigger.py b/tests/components/mqtt/test_trigger.py index a26ecadb6bd..b27af2b9bd0 100644 --- a/tests/components/mqtt/test_trigger.py +++ b/tests/components/mqtt/test_trigger.py @@ -1,11 +1,12 @@ """The tests for the MQTT automation.""" +from unittest.mock import ANY + import pytest import homeassistant.components.automation as automation from homeassistant.const import ATTR_ENTITY_ID, ENTITY_MATCH_ALL, SERVICE_TURN_OFF from homeassistant.setup import async_setup_component -from tests.async_mock import ANY from tests.common import async_fire_mqtt_message, async_mock_service, mock_component from tests.components.blueprint.conftest import stub_blueprint_populate # noqa diff --git a/tests/components/mqtt_eventstream/test_init.py b/tests/components/mqtt_eventstream/test_init.py index 87ac4696a31..da37489a130 100644 --- a/tests/components/mqtt_eventstream/test_init.py +++ b/tests/components/mqtt_eventstream/test_init.py @@ -1,5 +1,6 @@ """The tests for the MQTT eventstream component.""" import json +from unittest.mock import ANY, patch import homeassistant.components.mqtt_eventstream as eventstream from homeassistant.const import EVENT_STATE_CHANGED @@ -8,7 +9,6 @@ from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import ANY, patch from tests.common import ( async_fire_mqtt_message, async_fire_time_changed, diff --git a/tests/components/mqtt_json/test_device_tracker.py b/tests/components/mqtt_json/test_device_tracker.py index 89b37e28b52..d17484cc5e9 100644 --- a/tests/components/mqtt_json/test_device_tracker.py +++ b/tests/components/mqtt_json/test_device_tracker.py @@ -2,6 +2,7 @@ import json import logging import os +from unittest.mock import patch import pytest @@ -12,7 +13,6 @@ from homeassistant.components.device_tracker.legacy import ( from homeassistant.const import CONF_PLATFORM from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import async_fire_mqtt_message LOCATION_MESSAGE = { diff --git a/tests/components/mqtt_room/test_sensor.py b/tests/components/mqtt_room/test_sensor.py index e17fbb4847d..ca5f9420dc5 100644 --- a/tests/components/mqtt_room/test_sensor.py +++ b/tests/components/mqtt_room/test_sensor.py @@ -1,6 +1,7 @@ """The tests for the MQTT room presence sensor.""" import datetime import json +from unittest.mock import patch from homeassistant.components.mqtt import CONF_QOS, CONF_STATE_TOPIC, DEFAULT_QOS import homeassistant.components.sensor as sensor @@ -8,7 +9,6 @@ from homeassistant.const import CONF_NAME, CONF_PLATFORM from homeassistant.setup import async_setup_component from homeassistant.util import dt -from tests.async_mock import patch from tests.common import async_fire_mqtt_message DEVICE_ID = "123TESTMAC" diff --git a/tests/components/mqtt_statestream/test_init.py b/tests/components/mqtt_statestream/test_init.py index cea4b492f3e..d7bfcfe4f2e 100644 --- a/tests/components/mqtt_statestream/test_init.py +++ b/tests/components/mqtt_statestream/test_init.py @@ -1,9 +1,10 @@ """The tests for the MQTT statestream component.""" +from unittest.mock import ANY, call + import homeassistant.components.mqtt_statestream as statestream from homeassistant.core import State from homeassistant.setup import async_setup_component -from tests.async_mock import ANY, call from tests.common import mock_state_change_event diff --git a/tests/components/myq/test_config_flow.py b/tests/components/myq/test_config_flow.py index e2c43e8ce5c..bbfa090b01c 100644 --- a/tests/components/myq/test_config_flow.py +++ b/tests/components/myq/test_config_flow.py @@ -1,11 +1,12 @@ """Test the MyQ config flow.""" +from unittest.mock import patch + from pymyq.errors import InvalidCredentialsError, MyQError from homeassistant import config_entries, setup from homeassistant.components.myq.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry @@ -43,36 +44,6 @@ async def test_form_user(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_import(hass): - """Test we can import.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - with patch( - "homeassistant.components.myq.config_flow.pymyq.login", - return_value=True, - ), patch( - "homeassistant.components.myq.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.myq.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={"username": "test-username", "password": "test-password"}, - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == "test-username" - assert result["data"] == { - "username": "test-username", - "password": "test-password", - } - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - async def test_form_invalid_auth(hass): """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/myq/util.py b/tests/components/myq/util.py index 61c19325d57..84e85723918 100644 --- a/tests/components/myq/util.py +++ b/tests/components/myq/util.py @@ -1,12 +1,12 @@ """Tests for the myq integration.""" import json +from unittest.mock import patch from homeassistant.components.myq.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture diff --git a/tests/components/mythicbeastsdns/test_init.py b/tests/components/mythicbeastsdns/test_init.py index e8efac2c01d..01c9b253e4f 100644 --- a/tests/components/mythicbeastsdns/test_init.py +++ b/tests/components/mythicbeastsdns/test_init.py @@ -1,11 +1,10 @@ """Test the Mythic Beasts DNS component.""" import logging +from unittest.mock import patch from homeassistant.components import mythicbeastsdns from homeassistant.setup import async_setup_component -from tests.async_mock import patch - _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/neato/test_config_flow.py b/tests/components/neato/test_config_flow.py index 6954eb1b7af..7c7e25f2e0c 100644 --- a/tests/components/neato/test_config_flow.py +++ b/tests/components/neato/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Neato Botvac config flow.""" +from unittest.mock import patch + from pybotvac.neato import Neato from homeassistant import config_entries, data_entry_flow, setup @@ -6,7 +8,6 @@ from homeassistant.components.neato.const import NEATO_DOMAIN from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import patch from tests.common import MockConfigEntry CLIENT_ID = "1234" diff --git a/tests/components/ness_alarm/test_init.py b/tests/components/ness_alarm/test_init.py index f959b5345dd..507a786c3d1 100644 --- a/tests/components/ness_alarm/test_init.py +++ b/tests/components/ness_alarm/test_init.py @@ -1,5 +1,6 @@ """Tests for the ness_alarm component.""" from enum import Enum +from unittest.mock import MagicMock, patch import pytest @@ -31,8 +32,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, patch - VALID_CONFIG = { DOMAIN: { CONF_HOST: "alarm.local", diff --git a/tests/components/nest/camera_sdm_test.py b/tests/components/nest/camera_sdm_test.py index 69b413ba51a..84deef92d62 100644 --- a/tests/components/nest/camera_sdm_test.py +++ b/tests/components/nest/camera_sdm_test.py @@ -6,9 +6,11 @@ pubsub subscriber. """ import datetime +from unittest.mock import patch import aiohttp from google_nest_sdm.device import Device +from google_nest_sdm.event import EventMessage import pytest from homeassistant.components import camera @@ -18,7 +20,6 @@ from homeassistant.util.dt import utcnow from .common import async_setup_sdm_platform -from tests.async_mock import patch from tests.common import async_fire_time_changed PLATFORM = "camera" @@ -36,9 +37,71 @@ DEVICE_TRAITS = { "videoCodecs": ["H264"], "audioCodecs": ["AAC"], }, + "sdm.devices.traits.CameraEventImage": {}, + "sdm.devices.traits.CameraMotion": {}, } DATETIME_FORMAT = "YY-MM-DDTHH:MM:SS" DOMAIN = "nest" +MOTION_EVENT_ID = "FWWVQVUdGNUlTU2V4MGV2aTNXV..." + +# Tests can assert that image bytes came from an event or was decoded +# from the live stream. +IMAGE_BYTES_FROM_EVENT = b"test url image bytes" +IMAGE_BYTES_FROM_STREAM = b"test stream image bytes" + +TEST_IMAGE_URL = "https://domain/sdm_event_snapshot/dGTZwR3o4Y1..." +GENERATE_IMAGE_URL_RESPONSE = { + "results": { + "url": TEST_IMAGE_URL, + "token": "g.0.eventToken", + }, +} +IMAGE_AUTHORIZATION_HEADERS = {"Authorization": "Basic g.0.eventToken"} + + +def make_motion_event( + event_id: str = MOTION_EVENT_ID, timestamp: datetime.datetime = None +) -> EventMessage: + """Create an EventMessage for a motion event.""" + if not timestamp: + timestamp = utcnow() + return EventMessage( + { + "eventId": "some-event-id", # Ignored; we use the resource updated event id below + "timestamp": timestamp.isoformat(timespec="seconds"), + "resourceUpdate": { + "name": DEVICE_ID, + "events": { + "sdm.devices.events.CameraMotion.Motion": { + "eventSessionId": "CjY5Y3VKaTZwR3o4Y19YbTVfMF...", + "eventId": event_id, + }, + }, + }, + }, + auth=None, + ) + + +def make_stream_url_response( + expiration: datetime.datetime = None, token_num: int = 0 +) -> aiohttp.web.Response: + """Make response for the API that generates a streaming url.""" + if not expiration: + # Default to an arbitrary time in the future + expiration = utcnow() + datetime.timedelta(seconds=100) + return aiohttp.web.json_response( + { + "results": { + "streamUrls": { + "rtspUrl": f"rtsp://some/url?auth=g.{token_num}.streamingToken" + }, + "streamExtensionToken": f"g.{token_num}.extensionToken", + "streamToken": f"g.{token_num}.streamingToken", + "expiresAt": expiration.isoformat(timespec="seconds"), + }, + } + ) async def async_setup_camera(hass, traits={}, auth=None): @@ -63,6 +126,19 @@ async def fire_alarm(hass, point_in_time): await hass.async_block_till_done() +async def async_get_image(hass): + """Get image from the camera, a wrapper around camera.async_get_image.""" + # Note: this patches ImageFrame to simulate decoding an image from a live + # stream, however the test may not use it. Tests assert on the image + # contents to determine if the image came from the live stream or event. + with patch( + "homeassistant.components.ffmpeg.ImageFrame.get_image", + autopatch=True, + return_value=IMAGE_BYTES_FROM_STREAM, + ): + return await camera.async_get_image(hass, "camera.my_camera") + + async def test_no_devices(hass): """Test configuration that returns no devices.""" await async_setup_camera(hass) @@ -106,22 +182,7 @@ async def test_camera_device(hass): async def test_camera_stream(hass, auth): """Test a basic camera and fetch its live stream.""" - now = utcnow() - expiration = now + datetime.timedelta(seconds=100) - auth.responses = [ - aiohttp.web.json_response( - { - "results": { - "streamUrls": { - "rtspUrl": "rtsp://some/url?auth=g.0.streamingToken" - }, - "streamExtensionToken": "g.1.extensionToken", - "streamToken": "g.0.streamingToken", - "expiresAt": expiration.isoformat(timespec="seconds"), - }, - } - ) - ] + auth.responses = [make_stream_url_response()] await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) assert len(hass.states.async_all()) == 1 @@ -132,14 +193,8 @@ async def test_camera_stream(hass, auth): stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") assert stream_source == "rtsp://some/url?auth=g.0.streamingToken" - with patch( - "homeassistant.components.ffmpeg.ImageFrame.get_image", - autopatch=True, - return_value=b"image bytes", - ): - image = await camera.async_get_image(hass, "camera.my_camera") - - assert image.content == b"image bytes" + image = await async_get_image(hass) + assert image.content == IMAGE_BYTES_FROM_STREAM async def test_camera_stream_missing_trait(hass, auth): @@ -166,10 +221,9 @@ async def test_camera_stream_missing_trait(hass, auth): stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") assert stream_source is None - # Currently on support getting the image from a live stream + # Unable to get an image from the live stream with pytest.raises(HomeAssistantError): - image = await camera.async_get_image(hass, "camera.my_camera") - assert image is None + await async_get_image(hass) async def test_refresh_expired_stream_token(hass, auth): @@ -180,38 +234,11 @@ async def test_refresh_expired_stream_token(hass, auth): stream_3_expiration = now + datetime.timedelta(seconds=360) auth.responses = [ # Stream URL #1 - aiohttp.web.json_response( - { - "results": { - "streamUrls": { - "rtspUrl": "rtsp://some/url?auth=g.1.streamingToken" - }, - "streamExtensionToken": "g.1.extensionToken", - "streamToken": "g.1.streamingToken", - "expiresAt": stream_1_expiration.isoformat(timespec="seconds"), - }, - } - ), + make_stream_url_response(stream_1_expiration, token_num=1), # Stream URL #2 - aiohttp.web.json_response( - { - "results": { - "streamExtensionToken": "g.2.extensionToken", - "streamToken": "g.2.streamingToken", - "expiresAt": stream_2_expiration.isoformat(timespec="seconds"), - }, - } - ), + make_stream_url_response(stream_2_expiration, token_num=2), # Stream URL #3 - aiohttp.web.json_response( - { - "results": { - "streamExtensionToken": "g.3.extensionToken", - "streamToken": "g.3.streamingToken", - "expiresAt": stream_3_expiration.isoformat(timespec="seconds"), - }, - } - ), + make_stream_url_response(stream_3_expiration, token_num=3), ] await async_setup_camera( hass, @@ -258,36 +285,10 @@ async def test_stream_response_already_expired(hass, auth): stream_1_expiration = now + datetime.timedelta(seconds=-90) stream_2_expiration = now + datetime.timedelta(seconds=+90) auth.responses = [ - aiohttp.web.json_response( - { - "results": { - "streamUrls": { - "rtspUrl": "rtsp://some/url?auth=g.1.streamingToken" - }, - "streamExtensionToken": "g.1.extensionToken", - "streamToken": "g.1.streamingToken", - "expiresAt": stream_1_expiration.isoformat(timespec="seconds"), - }, - } - ), - aiohttp.web.json_response( - { - "results": { - "streamUrls": { - "rtspUrl": "rtsp://some/url?auth=g.2.streamingToken" - }, - "streamExtensionToken": "g.2.extensionToken", - "streamToken": "g.2.streamingToken", - "expiresAt": stream_2_expiration.isoformat(timespec="seconds"), - }, - } - ), + make_stream_url_response(stream_1_expiration, token_num=1), + make_stream_url_response(stream_2_expiration, token_num=2), ] - await async_setup_camera( - hass, - DEVICE_TRAITS, - auth=auth, - ) + await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) assert len(hass.states.async_all()) == 1 cam = hass.states.get("camera.my_camera") @@ -307,24 +308,7 @@ async def test_stream_response_already_expired(hass, auth): async def test_camera_removed(hass, auth): """Test case where entities are removed and stream tokens expired.""" - now = utcnow() - expiration = now + datetime.timedelta(seconds=100) - auth.responses = [ - aiohttp.web.json_response( - { - "results": { - "streamUrls": { - "rtspUrl": "rtsp://some/url?auth=g.0.streamingToken" - }, - "streamExtensionToken": "g.1.extensionToken", - "streamToken": "g.0.streamingToken", - "expiresAt": expiration.isoformat(timespec="seconds"), - }, - } - ), - aiohttp.web.json_response({"results": {}}), - ] - await async_setup_camera( + subscriber = await async_setup_camera( hass, DEVICE_TRAITS, auth=auth, @@ -335,9 +319,24 @@ async def test_camera_removed(hass, auth): assert cam is not None assert cam.state == STATE_IDLE + # Start a stream, exercising cleanup on remove + auth.responses = [ + make_stream_url_response(), + aiohttp.web.json_response({"results": {}}), + ] stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") assert stream_source == "rtsp://some/url?auth=g.0.streamingToken" + # Fetch an event image, exercising cleanup on remove + await subscriber.async_receive_event(make_motion_event()) + await hass.async_block_till_done() + auth.responses = [ + aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), + aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), + ] + image = await async_get_image(hass) + assert image.content == IMAGE_BYTES_FROM_EVENT + for config_entry in hass.config_entries.async_entries(DOMAIN): await hass.config_entries.async_remove(config_entry.entry_id) assert len(hass.states.async_all()) == 0 @@ -349,39 +348,13 @@ async def test_refresh_expired_stream_failure(hass, auth): stream_1_expiration = now + datetime.timedelta(seconds=90) stream_2_expiration = now + datetime.timedelta(seconds=180) auth.responses = [ - aiohttp.web.json_response( - { - "results": { - "streamUrls": { - "rtspUrl": "rtsp://some/url?auth=g.1.streamingToken" - }, - "streamExtensionToken": "g.1.extensionToken", - "streamToken": "g.1.streamingToken", - "expiresAt": stream_1_expiration.isoformat(timespec="seconds"), - }, - } - ), + make_stream_url_response(expiration=stream_1_expiration, token_num=1), # Extending the stream fails with arbitrary error aiohttp.web.Response(status=500), # Next attempt to get a stream fetches a new url - aiohttp.web.json_response( - { - "results": { - "streamUrls": { - "rtspUrl": "rtsp://some/url?auth=g.2.streamingToken" - }, - "streamExtensionToken": "g.2.extensionToken", - "streamToken": "g.2.streamingToken", - "expiresAt": stream_2_expiration.isoformat(timespec="seconds"), - }, - } - ), + make_stream_url_response(expiration=stream_2_expiration, token_num=2), ] - await async_setup_camera( - hass, - DEVICE_TRAITS, - auth=auth, - ) + await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) assert len(hass.states.async_all()) == 1 cam = hass.states.get("camera.my_camera") @@ -399,3 +372,191 @@ async def test_refresh_expired_stream_failure(hass, auth): # The stream is entirely refreshed stream_source = await camera.async_get_stream_source(hass, "camera.my_camera") assert stream_source == "rtsp://some/url?auth=g.2.streamingToken" + + +async def test_camera_image_from_last_event(hass, auth): + """Test an image generated from an event.""" + # The subscriber receives a message related to an image event. The camera + # holds on to the event message. When the test asks for a capera snapshot + # it exchanges the event id for an image url and fetches the image. + subscriber = await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) + assert len(hass.states.async_all()) == 1 + assert hass.states.get("camera.my_camera") + + # Simulate a pubsub message received by the subscriber with a motion event. + await subscriber.async_receive_event(make_motion_event()) + await hass.async_block_till_done() + + auth.responses = [ + # Fake response from API that returns url image + aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), + # Fake response for the image content fetch + aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), + ] + + image = await async_get_image(hass) + assert image.content == IMAGE_BYTES_FROM_EVENT + # Verify expected image fetch request was captured + assert auth.url == TEST_IMAGE_URL + assert auth.headers == IMAGE_AUTHORIZATION_HEADERS + + # An additional fetch uses the cache and does not send another RPC + image = await async_get_image(hass) + assert image.content == IMAGE_BYTES_FROM_EVENT + # Verify expected image fetch request was captured + assert auth.url == TEST_IMAGE_URL + assert auth.headers == IMAGE_AUTHORIZATION_HEADERS + + +async def test_camera_image_from_event_not_supported(hass, auth): + """Test fallback to stream image when event images are not supported.""" + # Create a device that does not support the CameraEventImgae trait + traits = DEVICE_TRAITS.copy() + del traits["sdm.devices.traits.CameraEventImage"] + subscriber = await async_setup_camera(hass, traits, auth=auth) + assert len(hass.states.async_all()) == 1 + assert hass.states.get("camera.my_camera") + + await subscriber.async_receive_event(make_motion_event()) + await hass.async_block_till_done() + + # Camera fetches a stream url since CameraEventImage is not supported + auth.responses = [make_stream_url_response()] + + image = await async_get_image(hass) + assert image.content == IMAGE_BYTES_FROM_STREAM + + +async def test_generate_event_image_url_failure(hass, auth): + """Test fallback to stream on failure to create an image url.""" + subscriber = await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) + assert len(hass.states.async_all()) == 1 + assert hass.states.get("camera.my_camera") + + await subscriber.async_receive_event(make_motion_event()) + await hass.async_block_till_done() + + auth.responses = [ + # Fail to generate the image url + aiohttp.web.Response(status=500), + # Camera fetches a stream url as a fallback + make_stream_url_response(), + ] + + image = await async_get_image(hass) + assert image.content == IMAGE_BYTES_FROM_STREAM + + +async def test_fetch_event_image_failure(hass, auth): + """Test fallback to a stream on image download failure.""" + subscriber = await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) + assert len(hass.states.async_all()) == 1 + assert hass.states.get("camera.my_camera") + + await subscriber.async_receive_event(make_motion_event()) + await hass.async_block_till_done() + + auth.responses = [ + # Fake response from API that returns url image + aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), + # Fail to download the image + aiohttp.web.Response(status=500), + # Camera fetches a stream url as a fallback + make_stream_url_response(), + ] + + image = await async_get_image(hass) + assert image.content == IMAGE_BYTES_FROM_STREAM + + +async def test_event_image_expired(hass, auth): + """Test fallback for an event event image that has expired.""" + subscriber = await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) + assert len(hass.states.async_all()) == 1 + assert hass.states.get("camera.my_camera") + + # Simulate a pubsub message has already expired + event_timestamp = utcnow() - datetime.timedelta(seconds=40) + await subscriber.async_receive_event(make_motion_event(timestamp=event_timestamp)) + await hass.async_block_till_done() + + # Fallback to a stream url since the event message is expired. + auth.responses = [make_stream_url_response()] + + image = await async_get_image(hass) + assert image.content == IMAGE_BYTES_FROM_STREAM + + +async def test_event_image_becomes_expired(hass, auth): + """Test fallback for an event event image that has been cleaned up on expiration.""" + subscriber = await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) + assert len(hass.states.async_all()) == 1 + assert hass.states.get("camera.my_camera") + + event_timestamp = utcnow() + await subscriber.async_receive_event(make_motion_event(timestamp=event_timestamp)) + await hass.async_block_till_done() + + auth.responses = [ + # Fake response from API that returns url image + aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), + # Fake response for the image content fetch + aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), + # Image is refetched after being cleared by expiration alarm + aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), + aiohttp.web.Response(body=b"updated image bytes"), + ] + + image = await async_get_image(hass) + assert image.content == IMAGE_BYTES_FROM_EVENT + + # Event image is still valid before expiration + next_update = event_timestamp + datetime.timedelta(seconds=25) + await fire_alarm(hass, next_update) + + image = await async_get_image(hass) + assert image.content == IMAGE_BYTES_FROM_EVENT + + # Fire an alarm well after expiration, removing image from cache + # Note: This test does not override the "now" logic within the underlying + # python library that tracks active events. Instead, it exercises the + # alarm behavior only. That is, the library may still think the event is + # active even though Home Assistant does not due to patching time. + next_update = event_timestamp + datetime.timedelta(seconds=180) + await fire_alarm(hass, next_update) + + image = await async_get_image(hass) + assert image.content == b"updated image bytes" + + +async def test_multiple_event_images(hass, auth): + """Test fallback for an event event image that has been cleaned up on expiration.""" + subscriber = await async_setup_camera(hass, DEVICE_TRAITS, auth=auth) + assert len(hass.states.async_all()) == 1 + assert hass.states.get("camera.my_camera") + + event_timestamp = utcnow() + await subscriber.async_receive_event(make_motion_event(timestamp=event_timestamp)) + await hass.async_block_till_done() + + auth.responses = [ + # Fake response from API that returns url image + aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), + # Fake response for the image content fetch + aiohttp.web.Response(body=IMAGE_BYTES_FROM_EVENT), + # Image is refetched after being cleared by expiration alarm + aiohttp.web.json_response(GENERATE_IMAGE_URL_RESPONSE), + aiohttp.web.Response(body=b"updated image bytes"), + ] + + image = await async_get_image(hass) + assert image.content == IMAGE_BYTES_FROM_EVENT + + next_event_timestamp = event_timestamp + datetime.timedelta(seconds=25) + await subscriber.async_receive_event( + make_motion_event(event_id="updated-event-id", timestamp=next_event_timestamp) + ) + await hass.async_block_till_done() + + image = await async_get_image(hass) + assert image.content == b"updated image bytes" diff --git a/tests/components/nest/climate_sdm_test.py b/tests/components/nest/climate_sdm_test.py index 886b67f8e2a..ef332d0e848 100644 --- a/tests/components/nest/climate_sdm_test.py +++ b/tests/components/nest/climate_sdm_test.py @@ -27,6 +27,7 @@ from homeassistant.components.climate.const import ( FAN_ON, HVAC_MODE_COOL, HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, @@ -699,6 +700,7 @@ async def test_thermostat_fan_off(hass): HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, HVAC_MODE_OFF, } assert thermostat.attributes[ATTR_FAN_MODE] == FAN_OFF @@ -730,13 +732,49 @@ async def test_thermostat_fan_on(hass): assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF + assert thermostat.state == HVAC_MODE_FAN_ONLY assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_OFF, + } + assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON + assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] + + +async def test_thermostat_cool_with_fan(hass): + """Test a thermostat cooling while the fan is on.""" + await setup_climate( + hass, + { + "sdm.devices.traits.Fan": { + "timerMode": "ON", + "timerTimeout": "2019-05-10T03:22:54Z", + }, + "sdm.devices.traits.ThermostatHvac": { + "status": "OFF", + }, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], + "mode": "COOL", + }, + }, + ) + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVAC_MODE_COOL + assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF + assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, HVAC_MODE_OFF, } assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON @@ -766,7 +804,7 @@ async def test_thermostat_set_fan(hass, auth): assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF + assert thermostat.state == HVAC_MODE_FAN_ONLY assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] @@ -845,13 +883,14 @@ async def test_thermostat_invalid_fan_mode(hass): assert len(hass.states.async_all()) == 1 thermostat = hass.states.get("climate.my_thermostat") assert thermostat is not None - assert thermostat.state == HVAC_MODE_OFF + assert thermostat.state == HVAC_MODE_FAN_ONLY assert thermostat.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_OFF assert thermostat.attributes[ATTR_CURRENT_TEMPERATURE] == 16.2 assert set(thermostat.attributes[ATTR_HVAC_MODES]) == { HVAC_MODE_HEAT, HVAC_MODE_COOL, HVAC_MODE_HEAT_COOL, + HVAC_MODE_FAN_ONLY, HVAC_MODE_OFF, } assert thermostat.attributes[ATTR_FAN_MODE] == FAN_ON @@ -862,6 +901,54 @@ async def test_thermostat_invalid_fan_mode(hass): await hass.async_block_till_done() +async def test_thermostat_set_hvac_fan_only(hass, auth): + """Test a thermostat enabling the fan via hvac_mode.""" + await setup_climate( + hass, + { + "sdm.devices.traits.Fan": { + "timerMode": "OFF", + "timerTimeout": "2019-05-10T03:22:54Z", + }, + "sdm.devices.traits.ThermostatHvac": { + "status": "OFF", + }, + "sdm.devices.traits.ThermostatMode": { + "availableModes": ["HEAT", "COOL", "HEATCOOL", "OFF"], + "mode": "OFF", + }, + }, + auth=auth, + ) + + assert len(hass.states.async_all()) == 1 + thermostat = hass.states.get("climate.my_thermostat") + assert thermostat is not None + assert thermostat.state == HVAC_MODE_OFF + assert thermostat.attributes[ATTR_FAN_MODE] == FAN_OFF + assert thermostat.attributes[ATTR_FAN_MODES] == [FAN_ON, FAN_OFF] + + await common.async_set_hvac_mode(hass, HVAC_MODE_FAN_ONLY) + await hass.async_block_till_done() + + assert len(auth.captured_requests) == 2 + + (method, url, json, headers) = auth.captured_requests.pop(0) + assert method == "post" + assert url == "some-device-id:executeCommand" + assert json == { + "command": "sdm.devices.commands.Fan.SetTimer", + "params": {"timerMode": "ON"}, + } + (method, url, json, headers) = auth.captured_requests.pop(0) + assert method == "post" + assert url == "some-device-id:executeCommand" + assert json == { + "command": "sdm.devices.commands.ThermostatMode.SetMode", + "params": {"mode": "OFF"}, + } + + async def test_thermostat_target_temp(hass, auth): """Test a thermostat changing hvac modes and affected on target temps.""" subscriber = await setup_climate( diff --git a/tests/components/nest/common.py b/tests/components/nest/common.py index 65a37563911..d6dc730ec11 100644 --- a/tests/components/nest/common.py +++ b/tests/components/nest/common.py @@ -2,37 +2,52 @@ import time from typing import Awaitable, Callable +from unittest.mock import patch from google_nest_sdm.device_manager import DeviceManager from google_nest_sdm.event import EventMessage from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber from homeassistant.components.nest import DOMAIN +from homeassistant.components.nest.const import SDM_SCOPES from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry +PROJECT_ID = "some-project-id" +CLIENT_ID = "some-client-id" +CLIENT_SECRET = "some-client-secret" + CONFIG = { "nest": { - "client_id": "some-client-id", - "client_secret": "some-client-secret", + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, # Required fields for using SDM API - "project_id": "some-project-id", + "project_id": PROJECT_ID, "subscriber_id": "projects/example/subscriptions/subscriber-id-9876", }, } -CONFIG_ENTRY_DATA = { - "sdm": {}, # Indicates new SDM API, not legacy API - "auth_implementation": "local", - "token": { - "expires_at": time.time() + 86400, - "access_token": { - "token": "some-token", +FAKE_TOKEN = "some-token" +FAKE_REFRESH_TOKEN = "some-refresh-token" + + +def create_config_entry(hass, token_expiration_time=None): + """Create a ConfigEntry and add it to Home Assistant.""" + if token_expiration_time is None: + token_expiration_time = time.time() + 86400 + config_entry_data = { + "sdm": {}, # Indicates new SDM API, not legacy API + "auth_implementation": "nest", + "token": { + "access_token": FAKE_TOKEN, + "refresh_token": FAKE_REFRESH_TOKEN, + "scope": " ".join(SDM_SCOPES), + "token_type": "Bearer", + "expires_at": token_expiration_time, }, - }, -} + } + MockConfigEntry(domain=DOMAIN, data=config_entry_data).add_to_hass(hass) class FakeDeviceManager(DeviceManager): @@ -86,7 +101,7 @@ class FakeSubscriber(GoogleNestSubscriber): async def async_setup_sdm_platform(hass, platform, devices={}, structures={}): """Set up the platform and prerequisites.""" - MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA).add_to_hass(hass) + create_config_entry(hass) device_manager = FakeDeviceManager(devices=devices, structures=structures) subscriber = FakeSubscriber(device_manager) with patch( diff --git a/tests/components/nest/conftest.py b/tests/components/nest/conftest.py index 7e183ab9c82..764f037d181 100644 --- a/tests/components/nest/conftest.py +++ b/tests/components/nest/conftest.py @@ -14,29 +14,31 @@ class FakeAuth(AbstractAuth): from the API. """ - # Tests can set fake responses here. - responses = [] - # The last request is recorded here. - method = None - url = None - json = None - - # Set up by fixture - client = None - def __init__(self): """Initialize FakeAuth.""" super().__init__(None, None) + # Tests can set fake responses here. + self.responses = [] + # The last request is recorded here. + self.method = None + self.url = None + self.json = None + self.headers = None + self.captured_requests = [] + # Set up by fixture + self.client = None async def async_get_access_token(self) -> str: """Return a valid access token.""" return "" - async def request(self, method, url, json): + async def request(self, method, url, **kwargs): """Capure the request arguments for tests to assert on.""" self.method = method self.url = url - self.json = json + self.json = kwargs.get("json") + self.headers = kwargs.get("headers") + self.captured_requests.append((method, url, self.json, self.headers)) return await self.client.get("/") async def response_handler(self, request): diff --git a/tests/components/nest/test_api.py b/tests/components/nest/test_api.py new file mode 100644 index 00000000000..835edf0c3a2 --- /dev/null +++ b/tests/components/nest/test_api.py @@ -0,0 +1,151 @@ +"""Tests for the Nest integration API glue library. + +There are two interesting cases to exercise that have different strategies +for token refresh and for testing: +- API based requests, tested using aioclient_mock +- Pub/sub subcriber initialization, intercepted with patch() + +The tests below exercise both cases during integration setup. +""" + +import time +from unittest.mock import patch + +from homeassistant.components.nest import DOMAIN +from homeassistant.components.nest.const import API_URL, OAUTH2_TOKEN, SDM_SCOPES +from homeassistant.setup import async_setup_component +from homeassistant.util import dt + +from .common import ( + CLIENT_ID, + CLIENT_SECRET, + CONFIG, + FAKE_REFRESH_TOKEN, + FAKE_TOKEN, + PROJECT_ID, + create_config_entry, +) + +FAKE_UPDATED_TOKEN = "fake-updated-token" + + +async def async_setup_sdm(hass): + """Set up the integration.""" + assert await async_setup_component(hass, DOMAIN, CONFIG) + await hass.async_block_till_done() + + +async def test_auth(hass, aioclient_mock): + """Exercise authentication library creates valid credentials.""" + + expiration_time = time.time() + 86400 + create_config_entry(hass, expiration_time) + + # Prepare to capture credentials in API request. Empty payloads just mean + # no devices or structures are loaded. + aioclient_mock.get(f"{API_URL}/enterprises/{PROJECT_ID}/structures", json={}) + aioclient_mock.get(f"{API_URL}/enterprises/{PROJECT_ID}/devices", json={}) + + # Prepare to capture credentials for Subscriber + captured_creds = None + + async def async_new_subscriber(creds, subscription_name, loop, async_callback): + """Capture credentials for tests.""" + nonlocal captured_creds + captured_creds = creds + return None # GoogleNestSubscriber + + with patch( + "google_nest_sdm.google_nest_subscriber.DefaultSubscriberFactory.async_new_subscriber", + side_effect=async_new_subscriber, + ) as new_subscriber_mock: + await async_setup_sdm(hass) + + # Verify API requests are made with the correct credentials + calls = aioclient_mock.mock_calls + assert len(calls) == 2 + (method, url, data, headers) = calls[0] + assert headers == {"Authorization": f"Bearer {FAKE_TOKEN}"} + (method, url, data, headers) = calls[1] + assert headers == {"Authorization": f"Bearer {FAKE_TOKEN}"} + + # Verify the susbcriber was created with the correct credentials + assert len(new_subscriber_mock.mock_calls) == 1 + assert captured_creds + creds = captured_creds + assert creds.token == FAKE_TOKEN + assert creds.refresh_token == FAKE_REFRESH_TOKEN + assert int(dt.as_timestamp(creds.expiry)) == int(expiration_time) + assert creds.valid + assert not creds.expired + assert creds.token_uri == OAUTH2_TOKEN + assert creds.client_id == CLIENT_ID + assert creds.client_secret == CLIENT_SECRET + assert creds.scopes == SDM_SCOPES + + +async def test_auth_expired_token(hass, aioclient_mock): + """Verify behavior of an expired token.""" + + expiration_time = time.time() - 86400 + create_config_entry(hass, expiration_time) + + # Prepare a token refresh response + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "access_token": FAKE_UPDATED_TOKEN, + "expires_at": time.time() + 86400, + "expires_in": 86400, + }, + ) + # Prepare to capture credentials in API request. Empty payloads just mean + # no devices or structures are loaded. + aioclient_mock.get(f"{API_URL}/enterprises/{PROJECT_ID}/structures", json={}) + aioclient_mock.get(f"{API_URL}/enterprises/{PROJECT_ID}/devices", json={}) + + # Prepare to capture credentials for Subscriber + captured_creds = None + + async def async_new_subscriber(creds, subscription_name, loop, async_callback): + """Capture credentials for tests.""" + nonlocal captured_creds + captured_creds = creds + return None # GoogleNestSubscriber + + with patch( + "google_nest_sdm.google_nest_subscriber.DefaultSubscriberFactory.async_new_subscriber", + side_effect=async_new_subscriber, + ) as new_subscriber_mock: + await async_setup_sdm(hass) + + calls = aioclient_mock.mock_calls + assert len(calls) == 3 + # Verify refresh token call to get an updated token + (method, url, data, headers) = calls[0] + assert data == { + "client_id": CLIENT_ID, + "client_secret": CLIENT_SECRET, + "grant_type": "refresh_token", + "refresh_token": FAKE_REFRESH_TOKEN, + } + # Verify API requests are made with the new token + (method, url, data, headers) = calls[1] + assert headers == {"Authorization": f"Bearer {FAKE_UPDATED_TOKEN}"} + (method, url, data, headers) = calls[2] + assert headers == {"Authorization": f"Bearer {FAKE_UPDATED_TOKEN}"} + + # The subscriber is created with a token that is expired. Verify that the + # credential is expired so the subscriber knows it needs to refresh it. + assert len(new_subscriber_mock.mock_calls) == 1 + assert captured_creds + creds = captured_creds + assert creds.token == FAKE_TOKEN + assert creds.refresh_token == FAKE_REFRESH_TOKEN + assert int(dt.as_timestamp(creds.expiry)) == int(expiration_time) + assert not creds.valid + assert creds.expired + assert creds.token_uri == OAUTH2_TOKEN + assert creds.client_id == CLIENT_ID + assert creds.client_secret == CLIENT_SECRET + assert creds.scopes == SDM_SCOPES diff --git a/tests/components/nest/test_config_flow_legacy.py b/tests/components/nest/test_config_flow_legacy.py index 23e01cf239a..ed4df2c7d84 100644 --- a/tests/components/nest/test_config_flow_legacy.py +++ b/tests/components/nest/test_config_flow_legacy.py @@ -1,12 +1,11 @@ """Tests for the Nest config flow.""" import asyncio -from unittest.mock import Mock, patch +from unittest.mock import AsyncMock, Mock, patch from homeassistant import data_entry_flow from homeassistant.components.nest import DOMAIN, config_flow from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock from tests.common import mock_coro diff --git a/tests/components/nest/test_config_flow_sdm.py b/tests/components/nest/test_config_flow_sdm.py index aad5621935e..f8c9c69698a 100644 --- a/tests/components/nest/test_config_flow_sdm.py +++ b/tests/components/nest/test_config_flow_sdm.py @@ -1,5 +1,7 @@ """Test the Google Nest Device Access config flow.""" +from unittest.mock import patch + import pytest from homeassistant import config_entries, setup @@ -9,8 +11,6 @@ from homeassistant.helpers import config_entry_oauth2_flow from .common import MockConfigEntry -from tests.async_mock import patch - CLIENT_ID = "1234" CLIENT_SECRET = "5678" PROJECT_ID = "project-id-4321" diff --git a/tests/components/nest/test_device_trigger.py b/tests/components/nest/test_device_trigger.py index b7c75862153..5e3b9bde442 100644 --- a/tests/components/nest/test_device_trigger.py +++ b/tests/components/nest/test_device_trigger.py @@ -102,9 +102,7 @@ async def test_get_triggers(hass): await async_setup_camera(hass, {DEVICE_ID: camera}) device_registry = await hass.helpers.device_registry.async_get_registry() - device_entry = device_registry.async_get_device( - {("nest", DEVICE_ID)}, connections={} - ) + device_entry = device_registry.async_get_device({("nest", DEVICE_ID)}) expected_triggers = [ { @@ -179,9 +177,7 @@ async def test_triggers_for_invalid_device_id(hass): await async_setup_camera(hass, {DEVICE_ID: camera}) device_registry = await hass.helpers.device_registry.async_get_registry() - device_entry = device_registry.async_get_device( - {("nest", DEVICE_ID)}, connections={} - ) + device_entry = device_registry.async_get_device({("nest", DEVICE_ID)}) assert device_entry is not None # Create an additional device that does not exist. Fetching supported @@ -293,9 +289,7 @@ async def test_subscriber_automation(hass, calls): subscriber = await async_setup_camera(hass, {DEVICE_ID: camera}) device_registry = await hass.helpers.device_registry.async_get_registry() - device_entry = device_registry.async_get_device( - {("nest", DEVICE_ID)}, connections={} - ) + device_entry = device_registry.async_get_device({("nest", DEVICE_ID)}) assert await setup_automation(hass, device_entry.id, "camera_motion") diff --git a/tests/components/nest/test_events.py b/tests/components/nest/test_events.py index 7295d134087..692507d6ff9 100644 --- a/tests/components/nest/test_events.py +++ b/tests/components/nest/test_events.py @@ -253,3 +253,41 @@ async def test_unknown_event(hass): await hass.async_block_till_done() assert len(events) == 0 + + +async def test_unknown_device_id(hass): + """Test a pubsub message for an unknown event type.""" + events = async_capture_events(hass, NEST_EVENT) + subscriber = await async_setup_devices( + hass, + "sdm.devices.types.DOORBELL", + create_device_traits("sdm.devices.traits.DoorbellChime"), + ) + await subscriber.async_receive_event( + create_event("sdm.devices.events.DoorbellChime.Chime", "invalid-device-id") + ) + await hass.async_block_till_done() + + assert len(events) == 0 + + +async def test_event_message_without_device_event(hass): + """Test a pubsub message for an unknown event type.""" + events = async_capture_events(hass, NEST_EVENT) + subscriber = await async_setup_devices( + hass, + "sdm.devices.types.DOORBELL", + create_device_traits("sdm.devices.traits.DoorbellChime"), + ) + timestamp = utcnow() + event = EventMessage( + { + "eventId": "some-event-id", + "timestamp": timestamp.isoformat(timespec="seconds"), + }, + auth=None, + ) + await subscriber.async_receive_event(event) + await hass.async_block_till_done() + + assert len(events) == 0 diff --git a/tests/components/nest/test_init_legacy.py b/tests/components/nest/test_init_legacy.py index f85fcdaa749..3a78877a235 100644 --- a/tests/components/nest/test_init_legacy.py +++ b/tests/components/nest/test_init_legacy.py @@ -1,10 +1,10 @@ """Test basic initialization for the Legacy Nest API using mocks for the Nest python library.""" import time +from unittest.mock import MagicMock, PropertyMock, patch from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, PropertyMock, patch from tests.common import MockConfigEntry DOMAIN = "nest" diff --git a/tests/components/nest/test_init_sdm.py b/tests/components/nest/test_init_sdm.py index cb17f81d18a..27bc02e3ea8 100644 --- a/tests/components/nest/test_init_sdm.py +++ b/tests/components/nest/test_init_sdm.py @@ -6,21 +6,20 @@ and failure modes. """ import logging +from unittest.mock import patch -from google_nest_sdm.exceptions import GoogleNestException +from google_nest_sdm.exceptions import AuthException, GoogleNestException from homeassistant.components.nest import DOMAIN from homeassistant.config_entries import ( ENTRY_STATE_LOADED, + ENTRY_STATE_NOT_LOADED, ENTRY_STATE_SETUP_ERROR, ENTRY_STATE_SETUP_RETRY, ) from homeassistant.setup import async_setup_component -from .common import CONFIG, CONFIG_ENTRY_DATA, async_setup_sdm_platform - -from tests.async_mock import patch -from tests.common import MockConfigEntry +from .common import CONFIG, async_setup_sdm_platform, create_config_entry PLATFORM = "sensor" @@ -38,11 +37,11 @@ async def test_setup_success(hass, caplog): async def async_setup_sdm(hass, config=CONFIG): """Prepare test setup.""" - MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA).add_to_hass(hass) + create_config_entry(hass) with patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation" ): - await async_setup_component(hass, DOMAIN, config) + return await async_setup_component(hass, DOMAIN, config) async def test_setup_configuration_failure(hass, caplog): @@ -50,7 +49,8 @@ async def test_setup_configuration_failure(hass, caplog): config = CONFIG.copy() config[DOMAIN]["subscriber_id"] = "invalid-subscriber-format" - await async_setup_sdm(hass, config) + result = await async_setup_sdm(hass, config) + assert result entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 @@ -67,7 +67,8 @@ async def test_setup_susbcriber_failure(hass, caplog): "homeassistant.components.nest.GoogleNestSubscriber.start_async", side_effect=GoogleNestException(), ), caplog.at_level(logging.ERROR, logger="homeassistant.components.nest"): - await async_setup_sdm(hass) + result = await async_setup_sdm(hass) + assert result assert "Subscriber error:" in caplog.text entries = hass.config_entries.async_entries(DOMAIN) @@ -81,10 +82,54 @@ async def test_setup_device_manager_failure(hass, caplog): "homeassistant.components.nest.GoogleNestSubscriber.async_get_device_manager", side_effect=GoogleNestException(), ), caplog.at_level(logging.ERROR, logger="homeassistant.components.nest"): - await async_setup_sdm(hass) + result = await async_setup_sdm(hass) + assert result assert len(caplog.messages) == 1 assert "Device manager error:" in caplog.text entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 assert entries[0].state == ENTRY_STATE_SETUP_RETRY + + +async def test_subscriber_auth_failure(hass, caplog): + """Test configuration error.""" + with patch( + "homeassistant.components.nest.GoogleNestSubscriber.start_async", + side_effect=AuthException(), + ): + result = await async_setup_sdm(hass, CONFIG) + assert result + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state == ENTRY_STATE_SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["step_id"] == "reauth_confirm" + + +async def test_setup_missing_subscriber_id(hass, caplog): + """Test successful setup.""" + config = CONFIG + del config[DOMAIN]["subscriber_id"] + with caplog.at_level(logging.ERROR, logger="homeassistant.components.nest"): + result = await async_setup_sdm(hass, config) + assert not result + assert "Configuration option" in caplog.text + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state == ENTRY_STATE_NOT_LOADED + + +async def test_empty_config(hass, caplog): + """Test successful setup.""" + with caplog.at_level(logging.ERROR, logger="homeassistant.components.nest"): + result = await async_setup_component(hass, DOMAIN, {}) + assert result + assert not caplog.records + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 0 diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py index 74a5d8dcc92..03c751aae96 100644 --- a/tests/components/netatmo/test_config_flow.py +++ b/tests/components/netatmo/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Netatmo config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.netatmo import config_flow from homeassistant.components.netatmo.const import ( @@ -11,7 +13,6 @@ from homeassistant.components.netatmo.const import ( from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow -from tests.async_mock import patch from tests.common import MockConfigEntry CLIENT_ID = "1234" diff --git a/tests/components/nexia/test_config_flow.py b/tests/components/nexia/test_config_flow.py index 81536f5beea..42917e77cfe 100644 --- a/tests/components/nexia/test_config_flow.py +++ b/tests/components/nexia/test_config_flow.py @@ -1,12 +1,12 @@ """Test the nexia config flow.""" +from unittest.mock import MagicMock, patch + from requests.exceptions import ConnectTimeout, HTTPError from homeassistant import config_entries, setup from homeassistant.components.nexia.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import MagicMock, patch - async def test_form(hass): """Test we get the form.""" @@ -139,45 +139,3 @@ async def test_form_broad_exception(hass): assert result2["type"] == "form" assert result2["errors"] == {"base": "unknown"} - - -async def test_form_import(hass): - """Test we get the form with import source.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - with patch( - "homeassistant.components.nexia.config_flow.NexiaHome.get_name", - return_value="myhouse", - ), patch( - "homeassistant.components.nexia.config_flow.NexiaHome.login", - side_effect=MagicMock(), - ), patch( - "homeassistant.components.nexia.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.nexia.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_USERNAME: "username", CONF_PASSWORD: "password"}, - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == "myhouse" - assert result["data"] == { - CONF_USERNAME: "username", - CONF_PASSWORD: "password", - } - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - result2 = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_USERNAME: "username", CONF_PASSWORD: "password"}, - ) - - assert result2["type"] == "abort" - assert result2["reason"] == "already_configured" diff --git a/tests/components/nexia/util.py b/tests/components/nexia/util.py index 7d34f0894e0..8e132941994 100644 --- a/tests/components/nexia/util.py +++ b/tests/components/nexia/util.py @@ -1,4 +1,5 @@ """Tests for the nexia integration.""" +from unittest.mock import patch import uuid from nexia.home import NexiaHome @@ -8,7 +9,6 @@ from homeassistant.components.nexia.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture diff --git a/tests/components/nextbus/test_sensor.py b/tests/components/nextbus/test_sensor.py index 4e7f0af8526..016afed2b0f 100644 --- a/tests/components/nextbus/test_sensor.py +++ b/tests/components/nextbus/test_sensor.py @@ -1,5 +1,6 @@ """The tests for the nexbus sensor component.""" from copy import deepcopy +from unittest.mock import patch import pytest @@ -7,7 +8,6 @@ import homeassistant.components.nextbus.sensor as nextbus import homeassistant.components.sensor as sensor from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import assert_setup_component VALID_AGENCY = "sf-muni" diff --git a/tests/components/nightscout/__init__.py b/tests/components/nightscout/__init__.py index c7f15068d1d..6c1a34ebe41 100644 --- a/tests/components/nightscout/__init__.py +++ b/tests/components/nightscout/__init__.py @@ -1,5 +1,6 @@ """Tests for the Nightscout integration.""" import json +from unittest.mock import patch from aiohttp import ClientConnectionError from py_nightscout.models import SGV, ServerStatus @@ -7,7 +8,6 @@ from py_nightscout.models import SGV, ServerStatus from homeassistant.components.nightscout.const import DOMAIN from homeassistant.const import CONF_URL -from tests.async_mock import patch from tests.common import MockConfigEntry GLUCOSE_READINGS = [ diff --git a/tests/components/nightscout/test_config_flow.py b/tests/components/nightscout/test_config_flow.py index 71983f1b29d..9a86e14b4e5 100644 --- a/tests/components/nightscout/test_config_flow.py +++ b/tests/components/nightscout/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Nightscout config flow.""" +from unittest.mock import patch + from aiohttp import ClientConnectionError, ClientResponseError from homeassistant import config_entries, data_entry_flow, setup @@ -6,7 +8,6 @@ from homeassistant.components.nightscout.const import DOMAIN from homeassistant.components.nightscout.utils import hash_from_url from homeassistant.const import CONF_URL -from tests.async_mock import patch from tests.common import MockConfigEntry from tests.components.nightscout import ( GLUCOSE_READINGS, diff --git a/tests/components/nightscout/test_init.py b/tests/components/nightscout/test_init.py index d81559ceba7..88ca141b999 100644 --- a/tests/components/nightscout/test_init.py +++ b/tests/components/nightscout/test_init.py @@ -1,4 +1,6 @@ """Test the Nightscout config flow.""" +from unittest.mock import patch + from aiohttp import ClientError from homeassistant.components.nightscout.const import DOMAIN @@ -9,7 +11,6 @@ from homeassistant.config_entries import ( ) from homeassistant.const import CONF_URL -from tests.async_mock import patch from tests.common import MockConfigEntry from tests.components.nightscout import init_integration diff --git a/tests/components/notify/test_init.py b/tests/components/notify/test_init.py new file mode 100644 index 00000000000..cce26750c1c --- /dev/null +++ b/tests/components/notify/test_init.py @@ -0,0 +1,83 @@ +"""The tests for notify services that change targets.""" +from homeassistant.components import notify +from homeassistant.core import HomeAssistant + + +async def test_same_targets(hass: HomeAssistant): + """Test not changing the targets in a notify service.""" + test = NotificationService(hass) + await test.async_setup(hass, "notify", "test") + await test.async_register_services() + await hass.async_block_till_done() + + assert hasattr(test, "registered_targets") + assert test.registered_targets == {"test_a": 1, "test_b": 2} + + await test.async_register_services() + await hass.async_block_till_done() + assert test.registered_targets == {"test_a": 1, "test_b": 2} + + +async def test_change_targets(hass: HomeAssistant): + """Test changing the targets in a notify service.""" + test = NotificationService(hass) + await test.async_setup(hass, "notify", "test") + await test.async_register_services() + await hass.async_block_till_done() + + assert hasattr(test, "registered_targets") + assert test.registered_targets == {"test_a": 1, "test_b": 2} + + test.target_list = {"a": 0} + await test.async_register_services() + await hass.async_block_till_done() + assert test.target_list == {"a": 0} + assert test.registered_targets == {"test_a": 0} + + +async def test_add_targets(hass: HomeAssistant): + """Test adding the targets in a notify service.""" + test = NotificationService(hass) + await test.async_setup(hass, "notify", "test") + await test.async_register_services() + await hass.async_block_till_done() + + assert hasattr(test, "registered_targets") + assert test.registered_targets == {"test_a": 1, "test_b": 2} + + test.target_list = {"a": 1, "b": 2, "c": 3} + await test.async_register_services() + await hass.async_block_till_done() + assert test.target_list == {"a": 1, "b": 2, "c": 3} + assert test.registered_targets == {"test_a": 1, "test_b": 2, "test_c": 3} + + +async def test_remove_targets(hass: HomeAssistant): + """Test removing targets from the targets in a notify service.""" + test = NotificationService(hass) + await test.async_setup(hass, "notify", "test") + await test.async_register_services() + await hass.async_block_till_done() + + assert hasattr(test, "registered_targets") + assert test.registered_targets == {"test_a": 1, "test_b": 2} + + test.target_list = {"c": 1} + await test.async_register_services() + await hass.async_block_till_done() + assert test.target_list == {"c": 1} + assert test.registered_targets == {"test_c": 1} + + +class NotificationService(notify.BaseNotificationService): + """A test class for notification services.""" + + def __init__(self, hass): + """Initialize the service.""" + self.hass = hass + self.target_list = {"a": 1, "b": 2} + + @property + def targets(self): + """Return a dictionary of devices.""" + return self.target_list diff --git a/tests/components/notion/test_config_flow.py b/tests/components/notion/test_config_flow.py index 728a8d40a52..d9ed37d516c 100644 --- a/tests/components/notion/test_config_flow.py +++ b/tests/components/notion/test_config_flow.py @@ -1,4 +1,6 @@ """Define tests for the Notion config flow.""" +from unittest.mock import AsyncMock, patch + import aionotion import pytest @@ -7,7 +9,6 @@ from homeassistant.components.notion import DOMAIN, config_flow from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry diff --git a/tests/components/nsw_fuel_station/test_sensor.py b/tests/components/nsw_fuel_station/test_sensor.py index e99a7bfc079..28ff7a7ed95 100644 --- a/tests/components/nsw_fuel_station/test_sensor.py +++ b/tests/components/nsw_fuel_station/test_sensor.py @@ -1,8 +1,9 @@ """The tests for the NSW Fuel Station sensor platform.""" +from unittest.mock import patch + from homeassistant.components import sensor from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import assert_setup_component VALID_CONFIG = { diff --git a/tests/components/nsw_rural_fire_service_feed/test_geo_location.py b/tests/components/nsw_rural_fire_service_feed/test_geo_location.py index b8923d854ee..d8719578957 100644 --- a/tests/components/nsw_rural_fire_service_feed/test_geo_location.py +++ b/tests/components/nsw_rural_fire_service_feed/test_geo_location.py @@ -1,5 +1,6 @@ """The tests for the NSW Rural Fire Service Feeds platform.""" import datetime +from unittest.mock import ANY, MagicMock, call, patch from aio_geojson_nsw_rfs_incidents import NswRuralFireServiceIncidentsFeed @@ -35,7 +36,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import ANY, MagicMock, call, patch from tests.common import assert_setup_component, async_fire_time_changed CONFIG = { diff --git a/tests/components/nuheat/mocks.py b/tests/components/nuheat/mocks.py index ccfb031f043..a5c2b403948 100644 --- a/tests/components/nuheat/mocks.py +++ b/tests/components/nuheat/mocks.py @@ -1,10 +1,16 @@ """The test for the NuHeat thermostat module.""" +from unittest.mock import MagicMock, Mock + from nuheat.config import SCHEDULE_HOLD, SCHEDULE_RUN, SCHEDULE_TEMPORARY_HOLD -from homeassistant.components.nuheat.const import DOMAIN +from homeassistant.components.nuheat.const import CONF_SERIAL_NUMBER, DOMAIN from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import MagicMock, Mock +MOCK_CONFIG_ENTRY = { + CONF_USERNAME: "me", + CONF_PASSWORD: "secret", + CONF_SERIAL_NUMBER: 12345, +} def _get_mock_thermostat_run(): diff --git a/tests/components/nuheat/test_climate.py b/tests/components/nuheat/test_climate.py index 453f0dab110..133c3a15ccb 100644 --- a/tests/components/nuheat/test_climate.py +++ b/tests/components/nuheat/test_climate.py @@ -1,22 +1,21 @@ """The test for the NuHeat thermostat module.""" from datetime import timedelta +from unittest.mock import patch from homeassistant.components.nuheat.const import DOMAIN from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from .mocks import ( + MOCK_CONFIG_ENTRY, _get_mock_nuheat, _get_mock_thermostat_run, _get_mock_thermostat_schedule_hold_available, _get_mock_thermostat_schedule_hold_unavailable, _get_mock_thermostat_schedule_temporary_hold, - _mock_get_config, ) -from tests.async_mock import patch -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed async def test_climate_thermostat_run(hass): @@ -28,7 +27,9 @@ async def test_climate_thermostat_run(hass): "homeassistant.components.nuheat.nuheat.NuHeat", return_value=mock_nuheat, ): - assert await async_setup_component(hass, DOMAIN, _mock_get_config()) + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_ENTRY) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() state = hass.states.get("climate.master_bathroom") @@ -59,7 +60,9 @@ async def test_climate_thermostat_schedule_hold_unavailable(hass): "homeassistant.components.nuheat.nuheat.NuHeat", return_value=mock_nuheat, ): - assert await async_setup_component(hass, DOMAIN, _mock_get_config()) + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_ENTRY) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() state = hass.states.get("climate.guest_bathroom") @@ -87,7 +90,9 @@ async def test_climate_thermostat_schedule_hold_available(hass): "homeassistant.components.nuheat.nuheat.NuHeat", return_value=mock_nuheat, ): - assert await async_setup_component(hass, DOMAIN, _mock_get_config()) + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_ENTRY) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() state = hass.states.get("climate.available_bathroom") @@ -119,7 +124,9 @@ async def test_climate_thermostat_schedule_temporary_hold(hass): "homeassistant.components.nuheat.nuheat.NuHeat", return_value=mock_nuheat, ): - assert await async_setup_component(hass, DOMAIN, _mock_get_config()) + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_ENTRY) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() state = hass.states.get("climate.temp_bathroom") diff --git a/tests/components/nuheat/test_config_flow.py b/tests/components/nuheat/test_config_flow.py index 5b2259faea8..a21e2e744de 100644 --- a/tests/components/nuheat/test_config_flow.py +++ b/tests/components/nuheat/test_config_flow.py @@ -1,4 +1,6 @@ """Test the NuHeat config flow.""" +from unittest.mock import MagicMock, patch + import requests from homeassistant import config_entries, setup @@ -7,8 +9,6 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, HTTP_INTERNAL_SERV from .mocks import _get_mock_thermostat_run -from tests.async_mock import MagicMock, patch - async def test_form_user(hass): """Test we get the form with user source.""" @@ -53,45 +53,6 @@ async def test_form_user(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_import(hass): - """Test we get the form with import source.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - mock_thermostat = _get_mock_thermostat_run() - - with patch( - "homeassistant.components.nuheat.config_flow.nuheat.NuHeat.authenticate", - return_value=True, - ), patch( - "homeassistant.components.nuheat.config_flow.nuheat.NuHeat.get_thermostat", - return_value=mock_thermostat, - ), patch( - "homeassistant.components.nuheat.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.nuheat.async_setup_entry", return_value=True - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_SERIAL_NUMBER: "12345", - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - }, - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == "Master bathroom" - assert result["data"] == { - CONF_SERIAL_NUMBER: "12345", - CONF_USERNAME: "test-username", - CONF_PASSWORD: "test-password", - } - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - async def test_form_invalid_auth(hass): """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/nuheat/test_init.py b/tests/components/nuheat/test_init.py index 8cd08f2edd5..093cd0573f6 100644 --- a/tests/components/nuheat/test_init.py +++ b/tests/components/nuheat/test_init.py @@ -1,10 +1,11 @@ """NuHeat component tests.""" +from unittest.mock import patch + from homeassistant.components.nuheat.const import DOMAIN -from homeassistant.setup import async_setup_component -from .mocks import _get_mock_nuheat +from .mocks import MOCK_CONFIG_ENTRY, _get_mock_nuheat -from tests.async_mock import patch +from tests.common import MockConfigEntry VALID_CONFIG = { "nuheat": {"username": "warm", "password": "feet", "devices": "thermostat123"} @@ -20,5 +21,7 @@ async def test_init_success(hass): "homeassistant.components.nuheat.nuheat.NuHeat", return_value=mock_nuheat, ): - assert await async_setup_component(hass, DOMAIN, VALID_CONFIG) + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG_ENTRY) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/number/test_device_action.py b/tests/components/number/test_device_action.py new file mode 100644 index 00000000000..21c79a9f741 --- /dev/null +++ b/tests/components/number/test_device_action.py @@ -0,0 +1,128 @@ +"""The tests for Number device actions.""" +import pytest +import voluptuous_serialize + +import homeassistant.components.automation as automation +from homeassistant.components.number import DOMAIN, device_action +from homeassistant.helpers import config_validation as cv, device_registry +from homeassistant.setup import async_setup_component + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_get_device_automations, + async_mock_service, + mock_device_registry, + mock_registry, +) +from tests.components.blueprint.conftest import stub_blueprint_populate # noqa + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +async def test_get_actions(hass, device_reg, entity_reg): + """Test we get the expected actions for an entity.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + hass.states.async_set("number.test_5678", 0.5, {"min_value": 0.0, "max_value": 1.0}) + expected_actions = [ + { + "domain": DOMAIN, + "type": "set_value", + "device_id": device_entry.id, + "entity_id": "number.test_5678", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert_lists_same(actions, expected_actions) + + +async def test_get_action_no_state(hass, device_reg, entity_reg): + """Test we get the expected actions for an entity.""" + config_entry = MockConfigEntry(domain="test", data={}) + config_entry.add_to_hass(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + entity_reg.async_get_or_create(DOMAIN, "test", "5678", device_id=device_entry.id) + expected_actions = [ + { + "domain": DOMAIN, + "type": "set_value", + "device_id": device_entry.id, + "entity_id": "number.test_5678", + }, + ] + actions = await async_get_device_automations(hass, "action", device_entry.id) + assert_lists_same(actions, expected_actions) + + +async def test_action(hass): + """Test for actions.""" + hass.states.async_set("number.entity", 0.5, {"min_value": 0.0, "max_value": 1.0}) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "event", + "event_type": "test_event_set_value", + }, + "action": { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "number.entity", + "type": "set_value", + "value": 0.3, + }, + }, + ] + }, + ) + + calls = async_mock_service(hass, DOMAIN, "set_value") + + assert len(calls) == 0 + + hass.bus.async_fire("test_event_set_value") + await hass.async_block_till_done() + + assert len(calls) == 1 + + +async def test_capabilities(hass): + """Test getting capabilities.""" + capabilities = await device_action.async_get_action_capabilities( + hass, + { + "domain": DOMAIN, + "device_id": "abcdefgh", + "entity_id": "number.entity", + "type": "set_value", + }, + ) + + assert capabilities and "extra_fields" in capabilities + + assert voluptuous_serialize.convert( + capabilities["extra_fields"], custom_serializer=cv.custom_serializer + ) == [{"name": "value", "required": True, "type": "float"}] diff --git a/tests/components/number/test_init.py b/tests/components/number/test_init.py index 58e090db20f..f1154581fdc 100644 --- a/tests/components/number/test_init.py +++ b/tests/components/number/test_init.py @@ -1,7 +1,7 @@ """The tests for the Number component.""" -from homeassistant.components.number import NumberEntity +from unittest.mock import MagicMock -from tests.async_mock import MagicMock +from homeassistant.components.number import NumberEntity class MockDefaultNumberEntity(NumberEntity): diff --git a/tests/components/nut/test_config_flow.py b/tests/components/nut/test_config_flow.py index e003ecd796b..bbe975a67c0 100644 --- a/tests/components/nut/test_config_flow.py +++ b/tests/components/nut/test_config_flow.py @@ -1,12 +1,13 @@ """Test the Network UPS Tools (NUT) config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.nut.const import DOMAIN from homeassistant.const import CONF_RESOURCES, CONF_SCAN_INTERVAL from .util import _get_mock_pynutclient -from tests.async_mock import patch from tests.common import MockConfigEntry VALID_CONFIG = { @@ -211,15 +212,41 @@ async def test_form_user_multiple_ups(hass): assert len(mock_setup_entry.mock_calls) == 2 -async def test_form_import(hass): - """Test we get the form with import source.""" +async def test_form_user_one_ups_with_ignored_entry(hass): + """Test we can setup a new one when there is an ignored one.""" + ignored_entry = MockConfigEntry( + domain=DOMAIN, data={}, source=config_entries.SOURCE_IGNORE + ) + ignored_entry.add_to_hass(hass) + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} mock_pynut = _get_mock_pynutclient( - list_vars={"battery.voltage": "serial"}, - list_ups={"ups1": "UPS 1", "ups2": "UPS2"}, + list_vars={"battery.voltage": "voltage", "ups.status": "OL"}, list_ups=["ups1"] ) + with patch( + "homeassistant.components.nut.PyNUTClient", + return_value=mock_pynut, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + "port": 2222, + }, + ) + + assert result2["step_id"] == "resources" + assert result2["type"] == "form" + with patch( "homeassistant.components.nut.PyNUTClient", return_value=mock_pynut, @@ -229,62 +256,25 @@ async def test_form_import(hass): "homeassistant.components.nut.async_setup_entry", return_value=True, ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - "host": "localhost", - "port": 123, - "name": "name", - "resources": ["battery.charge"], - }, + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {"resources": ["battery.voltage", "ups.status", "ups.status.display"]}, ) await hass.async_block_till_done() - assert result["type"] == "create_entry" - assert result["title"] == "localhost:123" - assert result["data"] == { - "host": "localhost", - "port": 123, - "name": "name", - "resources": ["battery.charge"], + assert result3["type"] == "create_entry" + assert result3["title"] == "1.1.1.1:2222" + assert result3["data"] == { + "host": "1.1.1.1", + "password": "test-password", + "port": 2222, + "resources": ["battery.voltage", "ups.status", "ups.status.display"], + "username": "test-username", } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_import_dupe(hass): - """Test we get abort on duplicate import.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - entry = MockConfigEntry(domain=DOMAIN, data=VALID_CONFIG) - entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=VALID_CONFIG - ) - assert result["type"] == "abort" - assert result["reason"] == "already_configured" - - -async def test_form_import_with_ignored_entry(hass): - """Test we get abort on duplicate import when there is an ignored one.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - entry = MockConfigEntry(domain=DOMAIN, data=VALID_CONFIG) - entry.add_to_hass(hass) - ignored_entry = MockConfigEntry( - domain=DOMAIN, data={}, source=config_entries.SOURCE_IGNORE - ) - ignored_entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=VALID_CONFIG - ) - assert result["type"] == "abort" - assert result["reason"] == "already_configured" - - async def test_form_cannot_connect(hass): """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/nut/util.py b/tests/components/nut/util.py index 772667b6b50..4e7506a9db1 100644 --- a/tests/components/nut/util.py +++ b/tests/components/nut/util.py @@ -1,12 +1,12 @@ """Tests for the nut integration.""" import json +from unittest.mock import MagicMock, patch from homeassistant.components.nut.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PORT, CONF_RESOURCES from homeassistant.core import HomeAssistant -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry, load_fixture diff --git a/tests/components/nws/conftest.py b/tests/components/nws/conftest.py index 74f84eb200c..d01201bb484 100644 --- a/tests/components/nws/conftest.py +++ b/tests/components/nws/conftest.py @@ -1,7 +1,8 @@ """Fixtures for National Weather Service tests.""" +from unittest.mock import AsyncMock, patch + import pytest -from tests.async_mock import AsyncMock, patch from tests.components.nws.const import DEFAULT_FORECAST, DEFAULT_OBSERVATION diff --git a/tests/components/nws/test_config_flow.py b/tests/components/nws/test_config_flow.py index 2ea5f36a379..81be7360e87 100644 --- a/tests/components/nws/test_config_flow.py +++ b/tests/components/nws/test_config_flow.py @@ -1,11 +1,11 @@ """Test the National Weather Service (NWS) config flow.""" +from unittest.mock import patch + import aiohttp from homeassistant import config_entries, setup from homeassistant.components.nws.const import DOMAIN -from tests.async_mock import patch - async def test_form(hass, mock_simple_nws_config): """Test we get the form.""" diff --git a/tests/components/nws/test_weather.py b/tests/components/nws/test_weather.py index bd8d81a4b0f..c7cb5b81c2a 100644 --- a/tests/components/nws/test_weather.py +++ b/tests/components/nws/test_weather.py @@ -1,5 +1,6 @@ """Tests for the NWS weather component.""" from datetime import timedelta +from unittest.mock import patch import aiohttp import pytest @@ -15,7 +16,6 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM, METRIC_SYSTEM -from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed from tests.components.nws.const import ( EXPECTED_FORECAST_IMPERIAL, diff --git a/tests/components/nzbget/__init__.py b/tests/components/nzbget/__init__.py index 27dc47a6df4..9993bdaff1e 100644 --- a/tests/components/nzbget/__init__.py +++ b/tests/components/nzbget/__init__.py @@ -1,5 +1,6 @@ """Tests for the NZBGet integration.""" from datetime import timedelta +from unittest.mock import patch from homeassistant.components.nzbget.const import DOMAIN from homeassistant.const import ( @@ -13,7 +14,6 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) -from tests.async_mock import patch from tests.common import MockConfigEntry ENTRY_CONFIG = { diff --git a/tests/components/nzbget/conftest.py b/tests/components/nzbget/conftest.py index 5855253b1d1..c9138cf59d3 100644 --- a/tests/components/nzbget/conftest.py +++ b/tests/components/nzbget/conftest.py @@ -1,10 +1,10 @@ """Define fixtures available for all tests.""" +from unittest.mock import MagicMock, patch + from pytest import fixture from . import MOCK_HISTORY, MOCK_STATUS, MOCK_VERSION -from tests.async_mock import MagicMock, patch - @fixture def nzbget_api(hass): diff --git a/tests/components/nzbget/test_config_flow.py b/tests/components/nzbget/test_config_flow.py index a58d1faa766..68488c376f6 100644 --- a/tests/components/nzbget/test_config_flow.py +++ b/tests/components/nzbget/test_config_flow.py @@ -1,4 +1,6 @@ """Test the NZBGet config flow.""" +from unittest.mock import patch + from pynzbgetapi import NZBGetAPIException from homeassistant.components.nzbget.const import DOMAIN @@ -21,7 +23,6 @@ from . import ( _patch_version, ) -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/nzbget/test_init.py b/tests/components/nzbget/test_init.py index d24a33d1f5b..2dcdab5754e 100644 --- a/tests/components/nzbget/test_init.py +++ b/tests/components/nzbget/test_init.py @@ -1,4 +1,6 @@ """Test the NZBGet config flow.""" +from unittest.mock import patch + from pynzbgetapi import NZBGetAPIException from homeassistant.components.nzbget.const import DOMAIN @@ -20,7 +22,6 @@ from . import ( init_integration, ) -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/nzbget/test_sensor.py b/tests/components/nzbget/test_sensor.py index c9f1ea71f0c..f5954bc7ee0 100644 --- a/tests/components/nzbget/test_sensor.py +++ b/tests/components/nzbget/test_sensor.py @@ -1,5 +1,6 @@ """Test the NZBGet sensors.""" from datetime import timedelta +from unittest.mock import patch from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, @@ -11,8 +12,6 @@ from homeassistant.util import dt as dt_util from . import init_integration -from tests.async_mock import patch - async def test_sensors(hass, nzbget_api) -> None: """Test the creation and values of the sensors.""" diff --git a/tests/components/omnilogic/test_config_flow.py b/tests/components/omnilogic/test_config_flow.py index 6243fe10efb..acf2df88610 100644 --- a/tests/components/omnilogic/test_config_flow.py +++ b/tests/components/omnilogic/test_config_flow.py @@ -1,10 +1,11 @@ """Test the Omnilogic config flow.""" +from unittest.mock import patch + from omnilogic import LoginException, OmniLogicException from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.omnilogic.const import DOMAIN -from tests.async_mock import patch from tests.common import MockConfigEntry DATA = {"username": "test-username", "password": "test-password"} diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index 73845aba7b2..4fa6b8da78a 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -1,6 +1,7 @@ """Test the onboarding views.""" import asyncio import os +from unittest.mock import patch import pytest @@ -11,7 +12,6 @@ from homeassistant.setup import async_setup_component from . import mock_storage -from tests.async_mock import patch from tests.common import CLIENT_ID, CLIENT_REDIRECT_URI, register_auth_provider from tests.components.met.conftest import mock_weather # noqa: F401 diff --git a/tests/components/ondilo_ico/__init__.py b/tests/components/ondilo_ico/__init__.py new file mode 100644 index 00000000000..12d8d3e2b9f --- /dev/null +++ b/tests/components/ondilo_ico/__init__.py @@ -0,0 +1 @@ +"""Tests for the Ondilo ICO integration.""" diff --git a/tests/components/ondilo_ico/test_config_flow.py b/tests/components/ondilo_ico/test_config_flow.py new file mode 100644 index 00000000000..69d69e06b7c --- /dev/null +++ b/tests/components/ondilo_ico/test_config_flow.py @@ -0,0 +1,84 @@ +"""Test the Ondilo ICO config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.ondilo_ico.const import ( + DOMAIN, + OAUTH2_AUTHORIZE, + OAUTH2_CLIENTID, + OAUTH2_CLIENTSECRET, + OAUTH2_TOKEN, +) +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.helpers import config_entry_oauth2_flow + +from tests.common import MockConfigEntry + +CLIENT_ID = OAUTH2_CLIENTID +CLIENT_SECRET = OAUTH2_CLIENTSECRET + + +async def test_abort_if_existing_entry(hass): + """Check flow abort when an entry already exist.""" + MockConfigEntry(domain=DOMAIN).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "single_instance_allowed" + + +async def test_full_flow( + hass, aiohttp_client, aioclient_mock, current_request_with_host +): + """Check full flow.""" + assert await setup.async_setup_component( + hass, + DOMAIN, + { + DOMAIN: {CONF_CLIENT_ID: CLIENT_ID, CONF_CLIENT_SECRET: CLIENT_SECRET}, + "http": {"base_url": "https://example.com"}, + }, + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt( + hass, + { + "flow_id": result["flow_id"], + "redirect_uri": "https://example.com/auth/external/callback", + }, + ) + + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + "&scope=api" + ) + + client = await aiohttp_client(hass.http.app) + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + with patch( + "homeassistant.components.ondilo_ico.async_setup_entry", return_value=True + ) as mock_setup: + await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/onewire/__init__.py b/tests/components/onewire/__init__.py index 39a3c438cf9..716e73747f1 100644 --- a/tests/components/onewire/__init__.py +++ b/tests/components/onewire/__init__.py @@ -1,5 +1,7 @@ """Tests for 1-Wire integration.""" +from unittest.mock import patch + from homeassistant.components.onewire.const import ( CONF_MOUNT_DIR, CONF_NAMES, @@ -11,7 +13,6 @@ from homeassistant.components.onewire.const import ( from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/onewire/test_binary_sensor.py b/tests/components/onewire/test_binary_sensor.py index be740b13fb5..aee1641c24f 100644 --- a/tests/components/onewire/test_binary_sensor.py +++ b/tests/components/onewire/test_binary_sensor.py @@ -1,5 +1,6 @@ """Tests for 1-Wire devices connected on OWServer.""" import copy +from unittest.mock import patch from pyownet.protocol import Error as ProtocolError import pytest @@ -11,7 +12,6 @@ from homeassistant.setup import async_setup_component from . import setup_onewire_patched_owserver_integration -from tests.async_mock import patch from tests.common import mock_registry MOCK_DEVICE_SENSORS = { diff --git a/tests/components/onewire/test_config_flow.py b/tests/components/onewire/test_config_flow.py index ba0ae090ed2..ea0b5e85dda 100644 --- a/tests/components/onewire/test_config_flow.py +++ b/tests/components/onewire/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for 1-Wire config flow.""" +from unittest.mock import patch + from pyownet import protocol from homeassistant.components.onewire.const import ( @@ -19,8 +21,6 @@ from homeassistant.data_entry_flow import ( from . import setup_onewire_owserver_integration, setup_onewire_sysbus_integration -from tests.async_mock import patch - async def test_user_owserver(hass): """Test OWServer user flow.""" diff --git a/tests/components/onewire/test_entity_owserver.py b/tests/components/onewire/test_entity_owserver.py index aee84f9fe2b..42cbf77711c 100644 --- a/tests/components/onewire/test_entity_owserver.py +++ b/tests/components/onewire/test_entity_owserver.py @@ -1,4 +1,6 @@ """Tests for 1-Wire devices connected on OWServer.""" +from unittest.mock import patch + from pyownet.protocol import Error as ProtocolError import pytest @@ -30,7 +32,6 @@ from homeassistant.setup import async_setup_component from . import setup_onewire_patched_owserver_integration -from tests.async_mock import patch from tests.common import mock_device_registry, mock_registry MOCK_DEVICE_SENSORS = { @@ -703,6 +704,52 @@ MOCK_DEVICE_SENSORS = { }, ], }, + "7E.111111111111": { + "inject_reads": [ + b"EDS", # read type + b"EDS0068", # read device_type - note EDS specific + ], + "device_info": { + "identifiers": {(DOMAIN, "7E.111111111111")}, + "manufacturer": "Maxim Integrated", + "model": "EDS", + "name": "7E.111111111111", + }, + SENSOR_DOMAIN: [ + { + "entity_id": "sensor.7e_111111111111_temperature", + "unique_id": "/7E.111111111111/EDS0068/temperature", + "injected_value": b" 13.9375", + "result": "13.9", + "unit": TEMP_CELSIUS, + "class": DEVICE_CLASS_TEMPERATURE, + }, + { + "entity_id": "sensor.7e_111111111111_pressure", + "unique_id": "/7E.111111111111/EDS0068/pressure", + "injected_value": b" 1012.21", + "result": "1012.2", + "unit": PRESSURE_MBAR, + "class": DEVICE_CLASS_PRESSURE, + }, + { + "entity_id": "sensor.7e_111111111111_illuminance", + "unique_id": "/7E.111111111111/EDS0068/light", + "injected_value": b" 65.8839", + "result": "65.9", + "unit": LIGHT_LUX, + "class": DEVICE_CLASS_ILLUMINANCE, + }, + { + "entity_id": "sensor.7e_111111111111_humidity", + "unique_id": "/7E.111111111111/EDS0068/humidity", + "injected_value": b" 41.375", + "result": "41.4", + "unit": PERCENTAGE, + "class": DEVICE_CLASS_HUMIDITY, + }, + ], + }, } @@ -744,7 +791,7 @@ async def test_owserver_setup_valid_device(owproxy, hass, device_id, platform): if len(expected_sensors) > 0: device_info = mock_device_sensor["device_info"] assert len(device_registry.devices) == 1 - registry_entry = device_registry.async_get_device({(DOMAIN, device_id)}, set()) + registry_entry = device_registry.async_get_device({(DOMAIN, device_id)}) assert registry_entry is not None assert registry_entry.identifiers == {(DOMAIN, device_id)} assert registry_entry.manufacturer == device_info["manufacturer"] diff --git a/tests/components/onewire/test_entity_sysbus.py b/tests/components/onewire/test_entity_sysbus.py index 3ec46c60837..61a38c10f73 100644 --- a/tests/components/onewire/test_entity_sysbus.py +++ b/tests/components/onewire/test_entity_sysbus.py @@ -1,4 +1,6 @@ """Tests for 1-Wire devices connected on SysBus.""" +from unittest.mock import patch + from pi1wire import InvalidCRCException, UnsupportResponseException import pytest @@ -7,7 +9,6 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import mock_device_registry, mock_registry MOCK_CONFIG = { @@ -156,7 +157,7 @@ async def test_onewiredirect_setup_valid_device(hass, device_id): if len(expected_sensors) > 0: device_info = mock_device_sensor["device_info"] assert len(device_registry.devices) == 1 - registry_entry = device_registry.async_get_device({(DOMAIN, device_id)}, set()) + registry_entry = device_registry.async_get_device({(DOMAIN, device_id)}) assert registry_entry is not None assert registry_entry.identifiers == {(DOMAIN, device_id)} assert registry_entry.manufacturer == device_info["manufacturer"] diff --git a/tests/components/onewire/test_init.py b/tests/components/onewire/test_init.py index 8edb5fa0178..38e97206698 100644 --- a/tests/components/onewire/test_init.py +++ b/tests/components/onewire/test_init.py @@ -1,4 +1,6 @@ """Tests for 1-Wire config flow.""" +from unittest.mock import patch + from pyownet.protocol import ConnError, OwnetError from homeassistant.components.onewire.const import CONF_TYPE_OWSERVER, DOMAIN @@ -12,7 +14,6 @@ from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE from . import setup_onewire_owserver_integration, setup_onewire_sysbus_integration -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/onewire/test_sensor.py b/tests/components/onewire/test_sensor.py index ad9580f34ed..9e91da01b21 100644 --- a/tests/components/onewire/test_sensor.py +++ b/tests/components/onewire/test_sensor.py @@ -1,4 +1,6 @@ """Tests for 1-Wire sensor platform.""" +from unittest.mock import patch + from pyownet.protocol import Error as ProtocolError import pytest @@ -8,7 +10,6 @@ from homeassistant.setup import async_setup_component from . import setup_onewire_patched_owserver_integration -from tests.async_mock import patch from tests.common import assert_setup_component, mock_registry MOCK_COUPLERS = { diff --git a/tests/components/onewire/test_switch.py b/tests/components/onewire/test_switch.py index 0c70ad3c9fc..1c778d4e264 100644 --- a/tests/components/onewire/test_switch.py +++ b/tests/components/onewire/test_switch.py @@ -1,5 +1,6 @@ """Tests for 1-Wire devices connected on OWServer.""" import copy +from unittest.mock import patch from pyownet.protocol import Error as ProtocolError import pytest @@ -11,7 +12,6 @@ from homeassistant.setup import async_setup_component from . import setup_onewire_patched_owserver_integration -from tests.async_mock import patch from tests.common import mock_registry MOCK_DEVICE_SENSORS = { diff --git a/tests/components/onvif/test_config_flow.py b/tests/components/onvif/test_config_flow.py index b8be96a123c..1802d211348 100644 --- a/tests/components/onvif/test_config_flow.py +++ b/tests/components/onvif/test_config_flow.py @@ -1,11 +1,12 @@ """Test ONVIF config flow.""" +from unittest.mock import AsyncMock, MagicMock, patch + from onvif.exceptions import ONVIFError from zeep.exceptions import Fault from homeassistant import config_entries, data_entry_flow from homeassistant.components.onvif import config_flow -from tests.async_mock import AsyncMock, MagicMock, patch from tests.common import MockConfigEntry URN = "urn:uuid:123456789" diff --git a/tests/components/openalpr_cloud/test_image_processing.py b/tests/components/openalpr_cloud/test_image_processing.py index 7f285d150b7..e1e9192e1de 100644 --- a/tests/components/openalpr_cloud/test_image_processing.py +++ b/tests/components/openalpr_cloud/test_image_processing.py @@ -1,12 +1,12 @@ """The tests for the openalpr cloud platform.""" import asyncio +from unittest.mock import PropertyMock, patch from homeassistant.components import camera, image_processing as ip from homeassistant.components.openalpr_cloud.image_processing import OPENALPR_API_URL from homeassistant.core import callback from homeassistant.setup import setup_component -from tests.async_mock import PropertyMock, patch from tests.common import assert_setup_component, get_test_home_assistant, load_fixture from tests.components.image_processing import common diff --git a/tests/components/openalpr_local/test_image_processing.py b/tests/components/openalpr_local/test_image_processing.py index d98c27490e8..0cde6e0454b 100644 --- a/tests/components/openalpr_local/test_image_processing.py +++ b/tests/components/openalpr_local/test_image_processing.py @@ -1,10 +1,11 @@ """The tests for the openalpr local platform.""" +from unittest.mock import MagicMock, PropertyMock, patch + import homeassistant.components.image_processing as ip from homeassistant.const import ATTR_ENTITY_PICTURE from homeassistant.core import callback from homeassistant.setup import setup_component -from tests.async_mock import MagicMock, PropertyMock, patch from tests.common import assert_setup_component, get_test_home_assistant, load_fixture from tests.components.image_processing import common diff --git a/tests/components/openerz/test_sensor.py b/tests/components/openerz/test_sensor.py index e616ea4fe4e..24a0f0610af 100644 --- a/tests/components/openerz/test_sensor.py +++ b/tests/components/openerz/test_sensor.py @@ -1,9 +1,9 @@ """Tests for OpenERZ component.""" +from unittest.mock import MagicMock, patch + from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, patch - MOCK_CONFIG = { "sensor": { "platform": "openerz", diff --git a/tests/components/opentherm_gw/test_config_flow.py b/tests/components/opentherm_gw/test_config_flow.py index e0f4c6eda99..4d811b9f985 100644 --- a/tests/components/opentherm_gw/test_config_flow.py +++ b/tests/components/opentherm_gw/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Opentherm Gateway config flow.""" import asyncio +from unittest.mock import patch from pyotgw.vars import OTGW, OTGW_ABOUT from serial import SerialException @@ -12,7 +13,6 @@ from homeassistant.components.opentherm_gw.const import ( ) from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_NAME, PRECISION_HALVES -from tests.async_mock import patch from tests.common import MockConfigEntry MINIMAL_STATUS = {OTGW: {OTGW_ABOUT: "OpenTherm Gateway 4.2.5"}} diff --git a/tests/components/opentherm_gw/test_init.py b/tests/components/opentherm_gw/test_init.py new file mode 100644 index 00000000000..b28f869e1e6 --- /dev/null +++ b/tests/components/opentherm_gw/test_init.py @@ -0,0 +1,69 @@ +"""Test Opentherm Gateway init.""" +from unittest.mock import patch + +from pyotgw.vars import OTGW, OTGW_ABOUT + +from homeassistant import setup +from homeassistant.components.opentherm_gw.const import DOMAIN +from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_NAME + +from tests.common import MockConfigEntry, mock_device_registry + +VERSION_OLD = "4.2.5" +VERSION_NEW = "4.2.8.1" +MINIMAL_STATUS = {OTGW: {OTGW_ABOUT: f"OpenTherm Gateway {VERSION_OLD}"}} +MINIMAL_STATUS_UPD = {OTGW: {OTGW_ABOUT: f"OpenTherm Gateway {VERSION_NEW}"}} +MOCK_GATEWAY_ID = "mock_gateway" +MOCK_CONFIG_ENTRY = MockConfigEntry( + domain=DOMAIN, + title="Mock Gateway", + data={ + CONF_NAME: "Mock Gateway", + CONF_DEVICE: "/dev/null", + CONF_ID: MOCK_GATEWAY_ID, + }, + options={}, +) + + +async def test_device_registry_insert(hass): + """Test that the device registry is initialized correctly.""" + MOCK_CONFIG_ENTRY.add_to_hass(hass) + + with patch( + "homeassistant.components.opentherm_gw.OpenThermGatewayDevice.cleanup", + return_value=None, + ), patch("pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS): + await setup.async_setup_component(hass, DOMAIN, {}) + + await hass.async_block_till_done() + + device_registry = await hass.helpers.device_registry.async_get_registry() + + gw_dev = device_registry.async_get_device(identifiers={(DOMAIN, MOCK_GATEWAY_ID)}) + assert gw_dev.sw_version == VERSION_OLD + + +async def test_device_registry_update(hass): + """Test that the device registry is updated correctly.""" + MOCK_CONFIG_ENTRY.add_to_hass(hass) + + dev_reg = mock_device_registry(hass) + dev_reg.async_get_or_create( + config_entry_id=MOCK_CONFIG_ENTRY.entry_id, + identifiers={(DOMAIN, MOCK_GATEWAY_ID)}, + name="Mock Gateway", + manufacturer="Schelte Bron", + model="OpenTherm Gateway", + sw_version=VERSION_OLD, + ) + + with patch( + "homeassistant.components.opentherm_gw.OpenThermGatewayDevice.cleanup", + return_value=None, + ), patch("pyotgw.pyotgw.connect", return_value=MINIMAL_STATUS_UPD): + await setup.async_setup_component(hass, DOMAIN, {}) + + await hass.async_block_till_done() + gw_dev = dev_reg.async_get_device(identifiers={(DOMAIN, MOCK_GATEWAY_ID)}) + assert gw_dev.sw_version == VERSION_NEW diff --git a/tests/components/openuv/test_config_flow.py b/tests/components/openuv/test_config_flow.py index 9c07322acca..83626c2d9f6 100644 --- a/tests/components/openuv/test_config_flow.py +++ b/tests/components/openuv/test_config_flow.py @@ -1,4 +1,6 @@ """Define tests for the OpenUV config flow.""" +from unittest.mock import patch + from pyopenuv.errors import InvalidApiKeyError import pytest @@ -12,7 +14,6 @@ from homeassistant.const import ( CONF_LONGITUDE, ) -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/openweathermap/test_config_flow.py b/tests/components/openweathermap/test_config_flow.py index c4d6be156bf..daa38bc1dc7 100644 --- a/tests/components/openweathermap/test_config_flow.py +++ b/tests/components/openweathermap/test_config_flow.py @@ -1,4 +1,6 @@ """Define tests for the OpenWeatherMap config flow.""" +from unittest.mock import MagicMock, patch + from pyowm.commons.exceptions import APIRequestError, UnauthorizedError from homeassistant import data_entry_flow @@ -17,7 +19,6 @@ from homeassistant.const import ( CONF_NAME, ) -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry CONFIG = { diff --git a/tests/components/ovo_energy/test_config_flow.py b/tests/components/ovo_energy/test_config_flow.py index a7f6ea9b9f2..ccf485211aa 100644 --- a/tests/components/ovo_energy/test_config_flow.py +++ b/tests/components/ovo_energy/test_config_flow.py @@ -1,4 +1,6 @@ """Test the OVO Energy config flow.""" +from unittest.mock import patch + import aiohttp from homeassistant import config_entries, data_entry_flow @@ -6,7 +8,6 @@ from homeassistant.components.ovo_energy.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -from tests.async_mock import patch from tests.common import MockConfigEntry FIXTURE_REAUTH_INPUT = {CONF_PASSWORD: "something1"} diff --git a/tests/components/owntracks/test_config_flow.py b/tests/components/owntracks/test_config_flow.py index 2290e3e17a4..d6ac059ce26 100644 --- a/tests/components/owntracks/test_config_flow.py +++ b/tests/components/owntracks/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for OwnTracks config flow.""" +from unittest.mock import patch + import pytest from homeassistant import data_entry_flow @@ -9,7 +11,6 @@ from homeassistant.config import async_process_ha_core_config from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry CONF_WEBHOOK_URL = "webhook_url" diff --git a/tests/components/owntracks/test_device_tracker.py b/tests/components/owntracks/test_device_tracker.py index 9f3694637f3..c21361c5fff 100644 --- a/tests/components/owntracks/test_device_tracker.py +++ b/tests/components/owntracks/test_device_tracker.py @@ -1,5 +1,6 @@ """The tests for the Owntracks device tracker.""" import json +from unittest.mock import patch import pytest @@ -7,7 +8,6 @@ from homeassistant.components import owntracks from homeassistant.const import STATE_NOT_HOME from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_mqtt_message, mock_coro USER = "greg" diff --git a/tests/components/owntracks/test_helper.py b/tests/components/owntracks/test_helper.py index 6d5139caa14..2c06ac0c4e7 100644 --- a/tests/components/owntracks/test_helper.py +++ b/tests/components/owntracks/test_helper.py @@ -1,10 +1,10 @@ """Test the owntracks_http platform.""" +from unittest.mock import patch + import pytest from homeassistant.components.owntracks import helper -from tests.async_mock import patch - @pytest.fixture(name="nacl_imported") def mock_nacl_imported(): diff --git a/tests/components/ozw/common.py b/tests/components/ozw/common.py index 7a78d11a445..1467d619afe 100644 --- a/tests/components/ozw/common.py +++ b/tests/components/ozw/common.py @@ -1,16 +1,17 @@ """Helpers for tests.""" import json +from unittest.mock import Mock, patch from homeassistant import config_entries from homeassistant.components.ozw.const import DOMAIN -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry async def setup_ozw(hass, entry=None, fixture=None): """Set up OZW and load a dump.""" - hass.config.components.add("mqtt") + mqtt_entry = MockConfigEntry(domain="mqtt", state=config_entries.ENTRY_STATE_LOADED) + mqtt_entry.add_to_hass(hass) if entry is None: entry = MockConfigEntry( diff --git a/tests/components/ozw/conftest.py b/tests/components/ozw/conftest.py index d3f8288658c..a59388f118f 100644 --- a/tests/components/ozw/conftest.py +++ b/tests/components/ozw/conftest.py @@ -1,12 +1,14 @@ """Helpers for tests.""" import json +from unittest.mock import patch import pytest +from homeassistant.config_entries import ENTRY_STATE_LOADED + from .common import MQTTMessage -from tests.async_mock import patch -from tests.common import load_fixture +from tests.common import MockConfigEntry, load_fixture from tests.components.light.conftest import mock_light_profiles # noqa @@ -16,6 +18,12 @@ def generic_data_fixture(): return load_fixture("ozw/generic_network_dump.csv") +@pytest.fixture(name="migration_data", scope="session") +def migration_data_fixture(): + """Load migration MQTT data and return it.""" + return load_fixture("ozw/migration_fixture.csv") + + @pytest.fixture(name="fan_data", scope="session") def fan_data_fixture(): """Load fan MQTT data and return it.""" @@ -262,3 +270,11 @@ def mock_get_addon_discovery_info(): "homeassistant.components.hassio.async_get_addon_discovery_info" ) as get_addon_discovery_info: yield get_addon_discovery_info + + +@pytest.fixture(name="mqtt") +async def mock_mqtt_fixture(hass): + """Mock the MQTT integration.""" + mqtt_entry = MockConfigEntry(domain="mqtt", state=ENTRY_STATE_LOADED) + mqtt_entry.add_to_hass(hass) + return mqtt_entry diff --git a/tests/components/ozw/test_config_flow.py b/tests/components/ozw/test_config_flow.py index e86232adc65..0a746398cf9 100644 --- a/tests/components/ozw/test_config_flow.py +++ b/tests/components/ozw/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Z-Wave over MQTT config flow.""" +from unittest.mock import patch + import pytest from homeassistant import config_entries, setup @@ -6,7 +8,6 @@ from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.ozw.config_flow import TITLE from homeassistant.components.ozw.const import DOMAIN -from tests.async_mock import patch from tests.common import MockConfigEntry ADDON_DISCOVERY_INFO = { @@ -78,9 +79,8 @@ def mock_start_addon(): yield start_addon -async def test_user_not_supervisor_create_entry(hass): +async def test_user_not_supervisor_create_entry(hass, mqtt): """Test the user step creates an entry not on Supervisor.""" - hass.config.components.add("mqtt") await setup.async_setup_component(hass, "persistent_notification", {}) with patch( @@ -127,9 +127,8 @@ async def test_one_instance_allowed(hass): assert result["reason"] == "single_instance_allowed" -async def test_not_addon(hass, supervisor): +async def test_not_addon(hass, supervisor, mqtt): """Test opting out of add-on on Supervisor.""" - hass.config.components.add("mqtt") await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( @@ -534,3 +533,52 @@ async def test_discovery_addon_not_installed( assert result["type"] == "form" assert result["step_id"] == "start_addon" + + +async def test_import_addon_installed( + hass, supervisor, addon_installed, addon_options, set_addon_options, start_addon +): + """Test add-on already installed but not running on Supervisor.""" + hass.config.components.add("mqtt") + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={"usb_path": "/test/imported", "network_key": "imported123"}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": True} + ) + + assert result["type"] == "form" + assert result["step_id"] == "start_addon" + + # the default input should be the imported data + default_input = result["data_schema"]({}) + + with patch( + "homeassistant.components.ozw.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.ozw.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], default_input + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == TITLE + assert result["data"] == { + "usb_path": "/test/imported", + "network_key": "imported123", + "use_addon": True, + "integration_created_addon": False, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/ozw/test_cover.py b/tests/components/ozw/test_cover.py index 07f7d76efb0..2b3b1e06862 100644 --- a/tests/components/ozw/test_cover.py +++ b/tests/components/ozw/test_cover.py @@ -16,6 +16,25 @@ async def test_cover(hass, cover_data, sent_messages, cover_msg): assert state.state == "closed" assert state.attributes[ATTR_CURRENT_POSITION] == 0 + # Test setting position + await hass.services.async_call( + "cover", + "set_cover_position", + {"entity_id": "cover.roller_shutter_3_instance_1_level", "position": 50}, + blocking=True, + ) + assert len(sent_messages) == 1 + msg = sent_messages[0] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": 50, "ValueIDKey": 625573905} + + # Feedback on state + cover_msg.decode() + cover_msg.payload["Value"] = 50 + cover_msg.encode() + receive_message(cover_msg) + await hass.async_block_till_done() + # Test opening await hass.services.async_call( "cover", @@ -23,22 +42,26 @@ async def test_cover(hass, cover_data, sent_messages, cover_msg): {"entity_id": "cover.roller_shutter_3_instance_1_level"}, blocking=True, ) - assert len(sent_messages) == 1 - msg = sent_messages[0] + assert len(sent_messages) == 2 + msg = sent_messages[1] assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 99, "ValueIDKey": 625573905} + assert msg["payload"] == {"Value": True, "ValueIDKey": 281475602284568} - # Feedback on state - cover_msg.decode() - cover_msg.payload["Value"] = 99 - cover_msg.encode() - receive_message(cover_msg) - await hass.async_block_till_done() + # Test stopping after opening + await hass.services.async_call( + "cover", + "stop_cover", + {"entity_id": "cover.roller_shutter_3_instance_1_level"}, + blocking=True, + ) + assert len(sent_messages) == 4 + msg = sent_messages[2] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": False, "ValueIDKey": 281475602284568} - state = hass.states.get("cover.roller_shutter_3_instance_1_level") - assert state is not None - assert state.state == "open" - assert state.attributes[ATTR_CURRENT_POSITION] == 100 + msg = sent_messages[3] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": False, "ValueIDKey": 562950578995224} # Test closing await hass.services.async_call( @@ -47,22 +70,43 @@ async def test_cover(hass, cover_data, sent_messages, cover_msg): {"entity_id": "cover.roller_shutter_3_instance_1_level"}, blocking=True, ) - assert len(sent_messages) == 2 - msg = sent_messages[1] + assert len(sent_messages) == 5 + msg = sent_messages[4] assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 0, "ValueIDKey": 625573905} + assert msg["payload"] == {"Value": True, "ValueIDKey": 562950578995224} - # Test setting position + # Test stopping after closing await hass.services.async_call( "cover", - "set_cover_position", - {"entity_id": "cover.roller_shutter_3_instance_1_level", "position": 50}, + "stop_cover", + {"entity_id": "cover.roller_shutter_3_instance_1_level"}, blocking=True, ) - assert len(sent_messages) == 3 - msg = sent_messages[2] + assert len(sent_messages) == 7 + msg = sent_messages[5] assert msg["topic"] == "OpenZWave/1/command/setvalue/" - assert msg["payload"] == {"Value": 50, "ValueIDKey": 625573905} + assert msg["payload"] == {"Value": False, "ValueIDKey": 281475602284568} + + msg = sent_messages[6] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": False, "ValueIDKey": 562950578995224} + + # Test stopping after no open/close + await hass.services.async_call( + "cover", + "stop_cover", + {"entity_id": "cover.roller_shutter_3_instance_1_level"}, + blocking=True, + ) + # both stop open/close messages sent + assert len(sent_messages) == 9 + msg = sent_messages[7] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": False, "ValueIDKey": 281475602284568} + + msg = sent_messages[8] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": False, "ValueIDKey": 562950578995224} # Test converting position to zwave range for position > 0 await hass.services.async_call( @@ -71,8 +115,8 @@ async def test_cover(hass, cover_data, sent_messages, cover_msg): {"entity_id": "cover.roller_shutter_3_instance_1_level", "position": 100}, blocking=True, ) - assert len(sent_messages) == 4 - msg = sent_messages[3] + assert len(sent_messages) == 10 + msg = sent_messages[9] assert msg["topic"] == "OpenZWave/1/command/setvalue/" assert msg["payload"] == {"Value": 99, "ValueIDKey": 625573905} @@ -83,8 +127,8 @@ async def test_cover(hass, cover_data, sent_messages, cover_msg): {"entity_id": "cover.roller_shutter_3_instance_1_level", "position": 0}, blocking=True, ) - assert len(sent_messages) == 5 - msg = sent_messages[4] + assert len(sent_messages) == 11 + msg = sent_messages[10] assert msg["topic"] == "OpenZWave/1/command/setvalue/" assert msg["payload"] == {"Value": 0, "ValueIDKey": 625573905} diff --git a/tests/components/ozw/test_init.py b/tests/components/ozw/test_init.py index efc38fa63c2..c76bfd4a3a0 100644 --- a/tests/components/ozw/test_init.py +++ b/tests/components/ozw/test_init.py @@ -1,11 +1,12 @@ """Test integration initialization.""" +from unittest.mock import patch + from homeassistant import config_entries from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.ozw import DOMAIN, PLATFORMS, const from .common import setup_ozw -from tests.async_mock import patch from tests.common import MockConfigEntry @@ -36,6 +37,26 @@ async def test_setup_entry_without_mqtt(hass): assert not await hass.config_entries.async_setup(entry.entry_id) +async def test_publish_without_mqtt(hass, caplog): + """Test publish without mqtt integration setup.""" + with patch("homeassistant.components.ozw.OZWOptions") as ozw_options: + await setup_ozw(hass) + + send_message = ozw_options.call_args[1]["send_message"] + + mqtt_entries = hass.config_entries.async_entries("mqtt") + mqtt_entry = mqtt_entries[0] + await hass.config_entries.async_remove(mqtt_entry.entry_id) + await hass.async_block_till_done() + + assert not hass.config_entries.async_entries("mqtt") + + # Sending a message should not error with the MQTT integration not set up. + send_message("test_topic", "test_payload") + + assert "MQTT integration is not set up" in caplog.text + + async def test_unload_entry(hass, generic_data, switch_msg, caplog): """Test unload the config entry.""" entry = MockConfigEntry( diff --git a/tests/components/ozw/test_migration.py b/tests/components/ozw/test_migration.py new file mode 100644 index 00000000000..d83a39f2b15 --- /dev/null +++ b/tests/components/ozw/test_migration.py @@ -0,0 +1,292 @@ +"""Test zwave to ozw migration.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.ozw.websocket_api import ID, TYPE +from homeassistant.helpers.device_registry import ( + DeviceEntry, + async_get_registry as async_get_device_registry, +) +from homeassistant.helpers.entity_registry import ( + RegistryEntry, + async_get_registry as async_get_entity_registry, +) + +from .common import setup_ozw + +from tests.common import MockConfigEntry, mock_device_registry, mock_registry + +ZWAVE_SOURCE_NODE_DEVICE_ID = "zwave_source_node_device_id" +ZWAVE_SOURCE_NODE_DEVICE_NAME = "Z-Wave Source Node Device" +ZWAVE_SOURCE_NODE_DEVICE_AREA = "Z-Wave Source Node Area" +ZWAVE_SOURCE_ENTITY = "sensor.zwave_source_node" +ZWAVE_SOURCE_NODE_UNIQUE_ID = "10-4321" +ZWAVE_BATTERY_DEVICE_ID = "zwave_battery_device_id" +ZWAVE_BATTERY_DEVICE_NAME = "Z-Wave Battery Device" +ZWAVE_BATTERY_DEVICE_AREA = "Z-Wave Battery Area" +ZWAVE_BATTERY_ENTITY = "sensor.zwave_battery_level" +ZWAVE_BATTERY_UNIQUE_ID = "36-1234" +ZWAVE_BATTERY_NAME = "Z-Wave Battery Level" +ZWAVE_BATTERY_ICON = "mdi:zwave-test-battery" +ZWAVE_POWER_DEVICE_ID = "zwave_power_device_id" +ZWAVE_POWER_DEVICE_NAME = "Z-Wave Power Device" +ZWAVE_POWER_DEVICE_AREA = "Z-Wave Power Area" +ZWAVE_POWER_ENTITY = "binary_sensor.zwave_power" +ZWAVE_POWER_UNIQUE_ID = "32-5678" +ZWAVE_POWER_NAME = "Z-Wave Power" +ZWAVE_POWER_ICON = "mdi:zwave-test-power" + + +@pytest.fixture(name="zwave_migration_data") +def zwave_migration_data_fixture(hass): + """Return mock zwave migration data.""" + zwave_source_node_device = DeviceEntry( + id=ZWAVE_SOURCE_NODE_DEVICE_ID, + name_by_user=ZWAVE_SOURCE_NODE_DEVICE_NAME, + area_id=ZWAVE_SOURCE_NODE_DEVICE_AREA, + ) + zwave_source_node_entry = RegistryEntry( + entity_id=ZWAVE_SOURCE_ENTITY, + unique_id=ZWAVE_SOURCE_NODE_UNIQUE_ID, + platform="zwave", + name="Z-Wave Source Node", + ) + zwave_battery_device = DeviceEntry( + id=ZWAVE_BATTERY_DEVICE_ID, + name_by_user=ZWAVE_BATTERY_DEVICE_NAME, + area_id=ZWAVE_BATTERY_DEVICE_AREA, + ) + zwave_battery_entry = RegistryEntry( + entity_id=ZWAVE_BATTERY_ENTITY, + unique_id=ZWAVE_BATTERY_UNIQUE_ID, + platform="zwave", + name=ZWAVE_BATTERY_NAME, + icon=ZWAVE_BATTERY_ICON, + ) + zwave_power_device = DeviceEntry( + id=ZWAVE_POWER_DEVICE_ID, + name_by_user=ZWAVE_POWER_DEVICE_NAME, + area_id=ZWAVE_POWER_DEVICE_AREA, + ) + zwave_power_entry = RegistryEntry( + entity_id=ZWAVE_POWER_ENTITY, + unique_id=ZWAVE_POWER_UNIQUE_ID, + platform="zwave", + name=ZWAVE_POWER_NAME, + icon=ZWAVE_POWER_ICON, + ) + zwave_migration_data = { + ZWAVE_SOURCE_NODE_UNIQUE_ID: { + "node_id": 10, + "node_instance": 1, + "device_id": zwave_source_node_device.id, + "command_class": 113, + "command_class_label": "SourceNodeId", + "value_index": 2, + "unique_id": ZWAVE_SOURCE_NODE_UNIQUE_ID, + "entity_entry": zwave_source_node_entry, + }, + ZWAVE_BATTERY_UNIQUE_ID: { + "node_id": 36, + "node_instance": 1, + "device_id": zwave_battery_device.id, + "command_class": 128, + "command_class_label": "Battery Level", + "value_index": 0, + "unique_id": ZWAVE_BATTERY_UNIQUE_ID, + "entity_entry": zwave_battery_entry, + }, + ZWAVE_POWER_UNIQUE_ID: { + "node_id": 32, + "node_instance": 1, + "device_id": zwave_power_device.id, + "command_class": 50, + "command_class_label": "Power", + "value_index": 8, + "unique_id": ZWAVE_POWER_UNIQUE_ID, + "entity_entry": zwave_power_entry, + }, + } + + mock_device_registry( + hass, + { + zwave_source_node_device.id: zwave_source_node_device, + zwave_battery_device.id: zwave_battery_device, + zwave_power_device.id: zwave_power_device, + }, + ) + mock_registry( + hass, + { + ZWAVE_SOURCE_ENTITY: zwave_source_node_entry, + ZWAVE_BATTERY_ENTITY: zwave_battery_entry, + ZWAVE_POWER_ENTITY: zwave_power_entry, + }, + ) + + return zwave_migration_data + + +@pytest.fixture(name="zwave_integration") +def zwave_integration_fixture(hass, zwave_migration_data): + """Mock the zwave integration.""" + hass.config.components.add("zwave") + zwave_config_entry = MockConfigEntry(domain="zwave", data={"usb_path": "/dev/test"}) + zwave_config_entry.add_to_hass(hass) + with patch( + "homeassistant.components.zwave.async_get_ozw_migration_data", + return_value=zwave_migration_data, + ): + yield zwave_config_entry + + +async def test_migrate_zwave(hass, migration_data, hass_ws_client, zwave_integration): + """Test the zwave to ozw migration websocket api.""" + await setup_ozw(hass, fixture=migration_data) + client = await hass_ws_client(hass) + + assert hass.config_entries.async_entries("zwave") + + await client.send_json({ID: 5, TYPE: "ozw/migrate_zwave", "dry_run": False}) + msg = await client.receive_json() + result = msg["result"] + + migration_entity_map = { + ZWAVE_BATTERY_ENTITY: "sensor.water_sensor_6_battery_level", + } + + assert result["zwave_entity_ids"] == [ + ZWAVE_SOURCE_ENTITY, + ZWAVE_BATTERY_ENTITY, + ZWAVE_POWER_ENTITY, + ] + assert result["ozw_entity_ids"] == [ + "sensor.smart_plug_electric_w", + "sensor.water_sensor_6_battery_level", + ] + assert result["migration_entity_map"] == migration_entity_map + assert result["migrated"] is True + + dev_reg = await async_get_device_registry(hass) + ent_reg = await async_get_entity_registry(hass) + + # check the device registry migration + + # check that the migrated entries have correct attributes + battery_entry = dev_reg.async_get_device( + identifiers={("ozw", "1.36.1")}, connections=set() + ) + assert battery_entry.name_by_user == ZWAVE_BATTERY_DEVICE_NAME + assert battery_entry.area_id == ZWAVE_BATTERY_DEVICE_AREA + power_entry = dev_reg.async_get_device( + identifiers={("ozw", "1.32.1")}, connections=set() + ) + assert power_entry.name_by_user == ZWAVE_POWER_DEVICE_NAME + assert power_entry.area_id == ZWAVE_POWER_DEVICE_AREA + + migration_device_map = { + ZWAVE_BATTERY_DEVICE_ID: battery_entry.id, + ZWAVE_POWER_DEVICE_ID: power_entry.id, + } + + assert result["migration_device_map"] == migration_device_map + + # check the entity registry migration + + # this should have been migrated and no longer present under that id + assert not ent_reg.async_is_registered("sensor.water_sensor_6_battery_level") + + # these should not have been migrated and is still in the registry + assert ent_reg.async_is_registered(ZWAVE_SOURCE_ENTITY) + source_entry = ent_reg.async_get(ZWAVE_SOURCE_ENTITY) + assert source_entry.unique_id == ZWAVE_SOURCE_NODE_UNIQUE_ID + assert ent_reg.async_is_registered(ZWAVE_POWER_ENTITY) + source_entry = ent_reg.async_get(ZWAVE_POWER_ENTITY) + assert source_entry.unique_id == ZWAVE_POWER_UNIQUE_ID + assert ent_reg.async_is_registered("sensor.smart_plug_electric_w") + + # this is the new entity_id of the ozw entity + assert ent_reg.async_is_registered(ZWAVE_BATTERY_ENTITY) + + # check that the migrated entries have correct attributes + battery_entry = ent_reg.async_get(ZWAVE_BATTERY_ENTITY) + assert battery_entry.unique_id == "1-36-610271249" + assert battery_entry.name == ZWAVE_BATTERY_NAME + assert battery_entry.icon == ZWAVE_BATTERY_ICON + + # check that the zwave config entry has been removed + assert not hass.config_entries.async_entries("zwave") + + # Check that the zwave integration fails entry setup after migration + zwave_config_entry = MockConfigEntry(domain="zwave") + zwave_config_entry.add_to_hass(hass) + assert not await hass.config_entries.async_setup(zwave_config_entry.entry_id) + + +async def test_migrate_zwave_dry_run( + hass, migration_data, hass_ws_client, zwave_integration +): + """Test the zwave to ozw migration websocket api dry run.""" + await setup_ozw(hass, fixture=migration_data) + client = await hass_ws_client(hass) + + await client.send_json({ID: 5, TYPE: "ozw/migrate_zwave"}) + msg = await client.receive_json() + result = msg["result"] + + migration_entity_map = { + ZWAVE_BATTERY_ENTITY: "sensor.water_sensor_6_battery_level", + } + + assert result["zwave_entity_ids"] == [ + ZWAVE_SOURCE_ENTITY, + ZWAVE_BATTERY_ENTITY, + ZWAVE_POWER_ENTITY, + ] + assert result["ozw_entity_ids"] == [ + "sensor.smart_plug_electric_w", + "sensor.water_sensor_6_battery_level", + ] + assert result["migration_entity_map"] == migration_entity_map + assert result["migrated"] is False + + ent_reg = await async_get_entity_registry(hass) + + # no real migration should have been done + assert ent_reg.async_is_registered("sensor.water_sensor_6_battery_level") + assert ent_reg.async_is_registered("sensor.smart_plug_electric_w") + + assert ent_reg.async_is_registered(ZWAVE_SOURCE_ENTITY) + source_entry = ent_reg.async_get(ZWAVE_SOURCE_ENTITY) + assert source_entry.unique_id == ZWAVE_SOURCE_NODE_UNIQUE_ID + + assert ent_reg.async_is_registered(ZWAVE_BATTERY_ENTITY) + battery_entry = ent_reg.async_get(ZWAVE_BATTERY_ENTITY) + assert battery_entry.unique_id == ZWAVE_BATTERY_UNIQUE_ID + + assert ent_reg.async_is_registered(ZWAVE_POWER_ENTITY) + power_entry = ent_reg.async_get(ZWAVE_POWER_ENTITY) + assert power_entry.unique_id == ZWAVE_POWER_UNIQUE_ID + + # check that the zwave config entry has not been removed + assert hass.config_entries.async_entries("zwave") + + # Check that the zwave integration can be setup after dry run + zwave_config_entry = zwave_integration + with patch("openzwave.option.ZWaveOption"), patch("openzwave.network.ZWaveNetwork"): + assert await hass.config_entries.async_setup(zwave_config_entry.entry_id) + + +async def test_migrate_zwave_not_setup(hass, migration_data, hass_ws_client): + """Test the zwave to ozw migration websocket without zwave setup.""" + await setup_ozw(hass, fixture=migration_data) + client = await hass_ws_client(hass) + + await client.send_json({ID: 5, TYPE: "ozw/migrate_zwave"}) + msg = await client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == "zwave_not_loaded" + assert msg["error"]["message"] == "Integration zwave is not loaded" diff --git a/tests/components/ozw/test_websocket_api.py b/tests/components/ozw/test_websocket_api.py index 37c6d184c8e..383d3425ffb 100644 --- a/tests/components/ozw/test_websocket_api.py +++ b/tests/components/ozw/test_websocket_api.py @@ -1,4 +1,6 @@ """Test OpenZWave Websocket API.""" +from unittest.mock import patch + from openzwavemqtt.const import ( ATTR_CODE_SLOT, ATTR_LABEL, @@ -40,8 +42,6 @@ from homeassistant.components.websocket_api.const import ( from .common import MQTTMessage, setup_ozw -from tests.async_mock import patch - async def test_websocket_api(hass, generic_data, hass_ws_client): """Test the ozw websocket api.""" diff --git a/tests/components/panasonic_viera/test_config_flow.py b/tests/components/panasonic_viera/test_config_flow.py index 15e6b73202d..e099862604a 100644 --- a/tests/components/panasonic_viera/test_config_flow.py +++ b/tests/components/panasonic_viera/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Panasonic Viera config flow.""" +from unittest.mock import Mock, patch + from panasonic_viera import TV_TYPE_ENCRYPTED, TV_TYPE_NONENCRYPTED, SOAPError import pytest @@ -21,7 +23,6 @@ from homeassistant.components.panasonic_viera.const import ( ) from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PIN, CONF_PORT -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/panasonic_viera/test_init.py b/tests/components/panasonic_viera/test_init.py index 3ac6b7e12da..8f95043f4fa 100644 --- a/tests/components/panasonic_viera/test_init.py +++ b/tests/components/panasonic_viera/test_init.py @@ -1,4 +1,6 @@ """Test the Panasonic Viera setup process.""" +from unittest.mock import Mock, patch + from homeassistant.components.panasonic_viera.const import ( ATTR_DEVICE_INFO, ATTR_FRIENDLY_NAME, @@ -18,7 +20,6 @@ from homeassistant.config_entries import ENTRY_STATE_NOT_LOADED from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry MOCK_CONFIG_DATA = { diff --git a/tests/components/panel_custom/test_init.py b/tests/components/panel_custom/test_init.py index ddcb4079ef7..bf67ca23e11 100644 --- a/tests/components/panel_custom/test_init.py +++ b/tests/components/panel_custom/test_init.py @@ -1,9 +1,9 @@ """The tests for the panel_custom component.""" +from unittest.mock import Mock, patch + from homeassistant import setup from homeassistant.components import frontend -from tests.async_mock import Mock, patch - async def test_webcomponent_custom_path_not_found(hass): """Test if a web component is found in config panels dir.""" diff --git a/tests/components/person/test_init.py b/tests/components/person/test_init.py index 64aa583e2f5..86ec71c1452 100644 --- a/tests/components/person/test_init.py +++ b/tests/components/person/test_init.py @@ -1,5 +1,6 @@ """The tests for the person component.""" import logging +from unittest.mock import patch import pytest @@ -24,7 +25,6 @@ from homeassistant.core import Context, CoreState, State from homeassistant.helpers import collection, entity_registry from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import mock_component, mock_restore_cache DEVICE_TRACKER = "device_tracker.test_tracker" diff --git a/tests/components/pi_hole/__init__.py b/tests/components/pi_hole/__init__.py index f487f413363..f02cd0c8a7a 100644 --- a/tests/components/pi_hole/__init__.py +++ b/tests/components/pi_hole/__init__.py @@ -1,7 +1,9 @@ """Tests for the pi_hole component.""" +from unittest.mock import AsyncMock, MagicMock, patch + from hole.exceptions import HoleError -from homeassistant.components.pi_hole.const import CONF_LOCATION +from homeassistant.components.pi_hole.const import CONF_LOCATION, CONF_STATISTICS_ONLY from homeassistant.const import ( CONF_API_KEY, CONF_HOST, @@ -11,8 +13,6 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) -from tests.async_mock import AsyncMock, MagicMock, patch - ZERO_DATA = { "ads_blocked_today": 0, "ads_percentage_today": 0, @@ -43,11 +43,25 @@ CONF_DATA = { CONF_VERIFY_SSL: VERIFY_SSL, } -CONF_CONFIG_FLOW = { +CONF_CONFIG_FLOW_USER = { CONF_HOST: HOST, CONF_PORT: PORT, CONF_LOCATION: LOCATION, CONF_NAME: NAME, + CONF_STATISTICS_ONLY: False, + CONF_SSL: SSL, + CONF_VERIFY_SSL: VERIFY_SSL, +} + +CONF_CONFIG_FLOW_API_KEY = { + CONF_API_KEY: API_KEY, +} + +CONF_CONFIG_ENTRY = { + CONF_HOST: f"{HOST}:{PORT}", + CONF_LOCATION: LOCATION, + CONF_NAME: NAME, + CONF_STATISTICS_ONLY: False, CONF_API_KEY: API_KEY, CONF_SSL: SSL, CONF_VERIFY_SSL: VERIFY_SSL, diff --git a/tests/components/pi_hole/test_config_flow.py b/tests/components/pi_hole/test_config_flow.py index 714f211d4f8..517697b0e8a 100644 --- a/tests/components/pi_hole/test_config_flow.py +++ b/tests/components/pi_hole/test_config_flow.py @@ -1,8 +1,10 @@ """Test pi_hole config flow.""" import logging +from unittest.mock import patch -from homeassistant.components.pi_hole.const import DOMAIN +from homeassistant.components.pi_hole.const import CONF_STATISTICS_ONLY, DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_API_KEY from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY, @@ -10,15 +12,15 @@ from homeassistant.data_entry_flow import ( ) from . import ( - CONF_CONFIG_FLOW, + CONF_CONFIG_ENTRY, + CONF_CONFIG_FLOW_API_KEY, + CONF_CONFIG_FLOW_USER, CONF_DATA, NAME, _create_mocked_hole, _patch_config_flow_hole, ) -from tests.async_mock import patch - def _flow_next(hass, flow_id): return next( @@ -44,7 +46,7 @@ async def test_flow_import(hass, caplog): ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["title"] == NAME - assert result["data"] == CONF_DATA + assert result["data"] == CONF_CONFIG_ENTRY # duplicated server result = await hass.config_entries.flow.async_init( @@ -81,28 +83,64 @@ async def test_flow_user(hass): result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input=CONF_CONFIG_FLOW, + user_input=CONF_CONFIG_FLOW_USER, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "api_key" + assert result["errors"] is None + _flow_next(hass, result["flow_id"]) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=CONF_CONFIG_FLOW_API_KEY, ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["title"] == NAME - assert result["data"] == CONF_DATA + assert result["data"] == CONF_CONFIG_ENTRY # duplicated server result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, - data=CONF_CONFIG_FLOW, + data=CONF_CONFIG_FLOW_USER, ) assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" +async def test_flow_statistics_only(hass): + """Test user initialized flow with statistics only.""" + mocked_hole = _create_mocked_hole() + with _patch_config_flow_hole(mocked_hole), _patch_setup(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + _flow_next(hass, result["flow_id"]) + + user_input = {**CONF_CONFIG_FLOW_USER} + user_input[CONF_STATISTICS_ONLY] = True + config_entry_data = {**CONF_CONFIG_ENTRY} + config_entry_data[CONF_STATISTICS_ONLY] = True + config_entry_data.pop(CONF_API_KEY) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=user_input, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == NAME + assert result["data"] == config_entry_data + + async def test_flow_user_invalid(hass): """Test user initialized flow with invalid server.""" mocked_hole = _create_mocked_hole(True) with _patch_config_flow_hole(mocked_hole), _patch_setup(): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW + DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW_USER ) assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "user" diff --git a/tests/components/pi_hole/test_init.py b/tests/components/pi_hole/test_init.py index 1f3e2451895..a14e155b3da 100644 --- a/tests/components/pi_hole/test_init.py +++ b/tests/components/pi_hole/test_init.py @@ -1,11 +1,13 @@ """Test pi_hole component.""" import logging +from unittest.mock import AsyncMock from hole.exceptions import HoleError from homeassistant.components import pi_hole, switch from homeassistant.components.pi_hole.const import ( CONF_LOCATION, + CONF_STATISTICS_ONLY, DEFAULT_LOCATION, DEFAULT_NAME, DEFAULT_SSL, @@ -15,6 +17,7 @@ from homeassistant.components.pi_hole.const import ( ) from homeassistant.const import ( ATTR_ENTITY_ID, + CONF_API_KEY, CONF_HOST, CONF_NAME, CONF_SSL, @@ -23,13 +26,14 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component from . import ( + CONF_CONFIG_ENTRY, + CONF_DATA, SWITCH_ENTITY_ID, _create_mocked_hole, _patch_config_flow_hole, _patch_init_hole, ) -from tests.async_mock import AsyncMock from tests.common import MockConfigEntry @@ -196,6 +200,7 @@ async def test_unload(hass): CONF_LOCATION: DEFAULT_LOCATION, CONF_SSL: DEFAULT_SSL, CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL, + CONF_STATISTICS_ONLY: True, }, ) entry.add_to_hass(hass) @@ -208,3 +213,34 @@ async def test_unload(hass): assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() assert entry.entry_id not in hass.data[pi_hole.DOMAIN] + + +async def test_migrate(hass): + """Test migrate from old config entry.""" + entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=CONF_DATA) + entry.add_to_hass(hass) + + mocked_hole = _create_mocked_hole() + with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.data == CONF_CONFIG_ENTRY + + +async def test_migrate_statistics_only(hass): + """Test migrate from old config entry with statistics only.""" + conf_data = {**CONF_DATA} + conf_data[CONF_API_KEY] = "" + entry = MockConfigEntry(domain=pi_hole.DOMAIN, data=conf_data) + entry.add_to_hass(hass) + + mocked_hole = _create_mocked_hole() + with _patch_config_flow_hole(mocked_hole), _patch_init_hole(mocked_hole): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + config_entry_data = {**CONF_CONFIG_ENTRY} + config_entry_data[CONF_STATISTICS_ONLY] = True + config_entry_data[CONF_API_KEY] = "" + assert entry.data == config_entry_data diff --git a/tests/components/pilight/test_init.py b/tests/components/pilight/test_init.py index 25ccb3dcf36..b69e03058bf 100644 --- a/tests/components/pilight/test_init.py +++ b/tests/components/pilight/test_init.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging import socket +from unittest.mock import patch from voluptuous import MultipleInvalid @@ -9,7 +10,6 @@ from homeassistant.components import pilight from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from tests.async_mock import patch from tests.common import assert_setup_component, async_fire_time_changed _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/ping/test_binary_sensor.py b/tests/components/ping/test_binary_sensor.py index ae6362939a7..a9af91c9f6f 100644 --- a/tests/components/ping/test_binary_sensor.py +++ b/tests/components/ping/test_binary_sensor.py @@ -1,12 +1,11 @@ """The test for the ping binary_sensor platform.""" from os import path +from unittest.mock import patch from homeassistant import config as hass_config, setup from homeassistant.components.ping import DOMAIN from homeassistant.const import SERVICE_RELOAD -from tests.async_mock import patch - async def test_reload(hass): """Verify we can reload trend sensors.""" diff --git a/tests/components/plex/conftest.py b/tests/components/plex/conftest.py index 1838df32a05..d3e66cc4989 100644 --- a/tests/components/plex/conftest.py +++ b/tests/components/plex/conftest.py @@ -1,14 +1,269 @@ """Fixtures for Plex tests.""" +from unittest.mock import patch + import pytest -from homeassistant.components.plex.const import DOMAIN +from homeassistant.components.plex.const import DOMAIN, PLEX_SERVER_CONFIG, SERVERS +from homeassistant.const import CONF_URL -from .const import DEFAULT_DATA, DEFAULT_OPTIONS +from .const import DEFAULT_DATA, DEFAULT_OPTIONS, PLEX_DIRECT_URL from .helpers import websocket_connected -from .mock_classes import MockGDM, MockPlexAccount, MockPlexServer +from .mock_classes import MockGDM -from tests.async_mock import patch -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_fixture + + +def plex_server_url(entry): + """Return a protocol-less URL from a config entry.""" + return entry.data[PLEX_SERVER_CONFIG][CONF_URL].split(":", 1)[-1] + + +@pytest.fixture(name="album", scope="session") +def album_fixture(): + """Load album payload and return it.""" + return load_fixture("plex/album.xml") + + +@pytest.fixture(name="artist_albums", scope="session") +def artist_albums_fixture(): + """Load artist's albums payload and return it.""" + return load_fixture("plex/artist_albums.xml") + + +@pytest.fixture(name="children_20", scope="session") +def children_20_fixture(): + """Load children payload for item 20 and return it.""" + return load_fixture("plex/children_20.xml") + + +@pytest.fixture(name="children_30", scope="session") +def children_30_fixture(): + """Load children payload for item 30 and return it.""" + return load_fixture("plex/children_30.xml") + + +@pytest.fixture(name="children_200", scope="session") +def children_200_fixture(): + """Load children payload for item 200 and return it.""" + return load_fixture("plex/children_200.xml") + + +@pytest.fixture(name="children_300", scope="session") +def children_300_fixture(): + """Load children payload for item 300 and return it.""" + return load_fixture("plex/children_300.xml") + + +@pytest.fixture(name="empty_library", scope="session") +def empty_library_fixture(): + """Load an empty library payload and return it.""" + return load_fixture("plex/empty_library.xml") + + +@pytest.fixture(name="empty_payload", scope="session") +def empty_payload_fixture(): + """Load an empty payload and return it.""" + return load_fixture("plex/empty_payload.xml") + + +@pytest.fixture(name="grandchildren_300", scope="session") +def grandchildren_300_fixture(): + """Load grandchildren payload for item 300 and return it.""" + return load_fixture("plex/grandchildren_300.xml") + + +@pytest.fixture(name="library_movies_all", scope="session") +def library_movies_all_fixture(): + """Load payload for all items in the movies library and return it.""" + return load_fixture("plex/library_movies_all.xml") + + +@pytest.fixture(name="library_tvshows_all", scope="session") +def library_tvshows_all_fixture(): + """Load payload for all items in the tvshows library and return it.""" + return load_fixture("plex/library_tvshows_all.xml") + + +@pytest.fixture(name="library_music_all", scope="session") +def library_music_all_fixture(): + """Load payload for all items in the music library and return it.""" + return load_fixture("plex/library_music_all.xml") + + +@pytest.fixture(name="library_movies_sort", scope="session") +def library_movies_sort_fixture(): + """Load sorting payload for movie library and return it.""" + return load_fixture("plex/library_movies_sort.xml") + + +@pytest.fixture(name="library_tvshows_sort", scope="session") +def library_tvshows_sort_fixture(): + """Load sorting payload for tvshow library and return it.""" + return load_fixture("plex/library_tvshows_sort.xml") + + +@pytest.fixture(name="library_music_sort", scope="session") +def library_music_sort_fixture(): + """Load sorting payload for music library and return it.""" + return load_fixture("plex/library_music_sort.xml") + + +@pytest.fixture(name="library", scope="session") +def library_fixture(): + """Load library payload and return it.""" + return load_fixture("plex/library.xml") + + +@pytest.fixture(name="library_sections", scope="session") +def library_sections_fixture(): + """Load library sections payload and return it.""" + return load_fixture("plex/library_sections.xml") + + +@pytest.fixture(name="media_1", scope="session") +def media_1_fixture(): + """Load media payload for item 1 and return it.""" + return load_fixture("plex/media_1.xml") + + +@pytest.fixture(name="media_30", scope="session") +def media_30_fixture(): + """Load media payload for item 30 and return it.""" + return load_fixture("plex/media_30.xml") + + +@pytest.fixture(name="media_100", scope="session") +def media_100_fixture(): + """Load media payload for item 100 and return it.""" + return load_fixture("plex/media_100.xml") + + +@pytest.fixture(name="media_200", scope="session") +def media_200_fixture(): + """Load media payload for item 200 and return it.""" + return load_fixture("plex/media_200.xml") + + +@pytest.fixture(name="player_plexweb_resources", scope="session") +def player_plexweb_resources_fixture(): + """Load resources payload for a Plex Web player and return it.""" + return load_fixture("plex/player_plexweb_resources.xml") + + +@pytest.fixture(name="playlists", scope="session") +def playlists_fixture(): + """Load payload for all playlists and return it.""" + return load_fixture("plex/playlists.xml") + + +@pytest.fixture(name="playlist_500", scope="session") +def playlist_500_fixture(): + """Load payload for playlist 500 and return it.""" + return load_fixture("plex/playlist_500.xml") + + +@pytest.fixture(name="playqueue_created", scope="session") +def playqueue_created_fixture(): + """Load payload for playqueue creation response and return it.""" + return load_fixture("plex/playqueue_created.xml") + + +@pytest.fixture(name="playqueue_1234", scope="session") +def playqueue_1234_fixture(): + """Load payload for playqueue 1234 and return it.""" + return load_fixture("plex/playqueue_1234.xml") + + +@pytest.fixture(name="plex_server_accounts", scope="session") +def plex_server_accounts_fixture(): + """Load payload accounts on the Plex server and return it.""" + return load_fixture("plex/plex_server_accounts.xml") + + +@pytest.fixture(name="plex_server_base", scope="session") +def plex_server_base_fixture(): + """Load base payload for Plex server info and return it.""" + return load_fixture("plex/plex_server_base.xml") + + +@pytest.fixture(name="plex_server_default", scope="session") +def plex_server_default_fixture(plex_server_base): + """Load default payload for Plex server info and return it.""" + return plex_server_base.format( + name="Plex Server 1", machine_identifier="unique_id_123" + ) + + +@pytest.fixture(name="plex_server_clients", scope="session") +def plex_server_clients_fixture(): + """Load available clients payload for Plex server and return it.""" + return load_fixture("plex/plex_server_clients.xml") + + +@pytest.fixture(name="plextv_account", scope="session") +def plextv_account_fixture(): + """Load account info from plex.tv and return it.""" + return load_fixture("plex/plextv_account.xml") + + +@pytest.fixture(name="plextv_resources_base", scope="session") +def plextv_resources_base_fixture(): + """Load base payload for plex.tv resources and return it.""" + return load_fixture("plex/plextv_resources_base.xml") + + +@pytest.fixture(name="plextv_resources", scope="session") +def plextv_resources_fixture(plextv_resources_base): + """Load default payload for plex.tv resources and return it.""" + return plextv_resources_base.format(second_server_enabled=0) + + +@pytest.fixture(name="session_base", scope="session") +def session_base_fixture(): + """Load the base session payload and return it.""" + return load_fixture("plex/session_base.xml") + + +@pytest.fixture(name="session_default", scope="session") +def session_default_fixture(session_base): + """Load the default session payload and return it.""" + return session_base.format(user_id=1) + + +@pytest.fixture(name="session_new_user", scope="session") +def session_new_user_fixture(session_base): + """Load the new user session payload and return it.""" + return session_base.format(user_id=1001) + + +@pytest.fixture(name="session_photo", scope="session") +def session_photo_fixture(): + """Load a photo session payload and return it.""" + return load_fixture("plex/session_photo.xml") + + +@pytest.fixture(name="session_plexweb", scope="session") +def session_plexweb_fixture(): + """Load a Plex Web session payload and return it.""" + return load_fixture("plex/session_plexweb.xml") + + +@pytest.fixture(name="security_token", scope="session") +def security_token_fixture(): + """Load a security token payload and return it.""" + return load_fixture("plex/security_token.xml") + + +@pytest.fixture(name="show_seasons", scope="session") +def show_seasons_fixture(): + """Load a show's seasons payload and return it.""" + return load_fixture("plex/show_seasons.xml") + + +@pytest.fixture(name="sonos_resources", scope="session") +def sonos_resources_fixture(): + """Load Sonos resources payload and return it.""" + return load_fixture("plex/sonos_resources.xml") @pytest.fixture(name="entry") @@ -22,14 +277,6 @@ def mock_config_entry(): ) -@pytest.fixture -def mock_plex_account(): - """Mock the PlexAccount class and return the used instance.""" - plex_account = MockPlexAccount() - with patch("plexapi.myplex.MyPlexAccount", return_value=plex_account): - yield plex_account - - @pytest.fixture def mock_websocket(): """Mock the PlexWebsocket class.""" @@ -38,15 +285,112 @@ def mock_websocket(): @pytest.fixture -def setup_plex_server(hass, entry, mock_plex_account, mock_websocket): +def mock_plex_calls( + entry, + requests_mock, + children_20, + children_30, + children_200, + children_300, + empty_library, + grandchildren_300, + library, + library_sections, + library_movies_all, + library_movies_sort, + library_music_all, + library_music_sort, + library_tvshows_all, + library_tvshows_sort, + media_1, + media_30, + media_100, + media_200, + playlists, + playlist_500, + plextv_account, + plextv_resources, + plex_server_accounts, + plex_server_clients, + plex_server_default, + security_token, +): + """Mock Plex API calls.""" + requests_mock.get("https://plex.tv/users/account", text=plextv_account) + requests_mock.get("https://plex.tv/api/resources", text=plextv_resources) + + url = plex_server_url(entry) + + for server in [url, PLEX_DIRECT_URL]: + requests_mock.get(server, text=plex_server_default) + requests_mock.get(f"{server}/accounts", text=plex_server_accounts) + + requests_mock.get(f"{url}/clients", text=plex_server_clients) + requests_mock.get(f"{url}/library", text=library) + requests_mock.get(f"{url}/library/sections", text=library_sections) + + requests_mock.get(f"{url}/library/onDeck", text=empty_library) + requests_mock.get(f"{url}/library/sections/1/sorts", text=library_movies_sort) + requests_mock.get(f"{url}/library/sections/2/sorts", text=library_tvshows_sort) + requests_mock.get(f"{url}/library/sections/3/sorts", text=library_music_sort) + + requests_mock.get(f"{url}/library/sections/1/all", text=library_movies_all) + requests_mock.get(f"{url}/library/sections/2/all", text=library_tvshows_all) + requests_mock.get(f"{url}/library/sections/3/all", text=library_music_all) + + requests_mock.get(f"{url}/library/metadata/200/children", text=children_200) + requests_mock.get(f"{url}/library/metadata/300/children", text=children_300) + requests_mock.get(f"{url}/library/metadata/300/allLeaves", text=grandchildren_300) + + requests_mock.get(f"{url}/library/metadata/1", text=media_1) + requests_mock.get(f"{url}/library/metadata/30", text=media_30) + requests_mock.get(f"{url}/library/metadata/100", text=media_100) + requests_mock.get(f"{url}/library/metadata/200", text=media_200) + + requests_mock.get(f"{url}/library/metadata/20/children", text=children_20) + requests_mock.get(f"{url}/library/metadata/30/children", text=children_30) + + requests_mock.get(f"{url}/playlists", text=playlists) + requests_mock.get(f"{url}/playlists/500/items", text=playlist_500) + requests_mock.get(f"{url}/security/token", text=security_token) + + +@pytest.fixture +def setup_plex_server( + hass, + entry, + mock_websocket, + mock_plex_calls, + requests_mock, + empty_payload, + session_default, + session_photo, + session_plexweb, +): """Set up and return a mocked Plex server instance.""" async def _wrapper(**kwargs): - """Wrap the fixture to allow passing arguments to the MockPlexServer instance.""" + """Wrap the fixture to allow passing arguments to the setup method.""" config_entry = kwargs.get("config_entry", entry) + disable_clients = kwargs.pop("disable_clients", False) disable_gdm = kwargs.pop("disable_gdm", True) - plex_server = MockPlexServer(**kwargs) - with patch("plexapi.server.PlexServer", return_value=plex_server), patch( + client_type = kwargs.pop("client_type", None) + session_type = kwargs.pop("session_type", None) + + if client_type == "plexweb": + session = session_plexweb + elif session_type == "photo": + session = session_photo + else: + session = session_default + + url = plex_server_url(entry) + requests_mock.get(f"{url}/status/sessions", text=session) + + if disable_clients: + requests_mock.get(f"{url}/clients", text=empty_payload) + + with patch( "homeassistant.components.plex.GDM", return_value=MockGDM(disabled=disable_gdm), ): @@ -55,6 +399,8 @@ def setup_plex_server(hass, entry, mock_plex_account, mock_websocket): await hass.async_block_till_done() websocket_connected(mock_websocket) await hass.async_block_till_done() + + plex_server = hass.data[DOMAIN][SERVERS][entry.unique_id] return plex_server return _wrapper diff --git a/tests/components/plex/const.py b/tests/components/plex/const.py index 548be2edeb8..9e376d19cac 100644 --- a/tests/components/plex/const.py +++ b/tests/components/plex/const.py @@ -61,3 +61,5 @@ DEFAULT_OPTIONS = { const.CONF_USE_EPISODE_ART: False, } } + +PLEX_DIRECT_URL = "https://1-2-3-4.123456789001234567890.plex.direct:32400" diff --git a/tests/components/plex/mock_classes.py b/tests/components/plex/mock_classes.py index 8ac894438be..c6f1aeda9b7 100644 --- a/tests/components/plex/mock_classes.py +++ b/tests/components/plex/mock_classes.py @@ -1,17 +1,4 @@ """Mock classes used in tests.""" -from functools import lru_cache - -from aiohttp.helpers import reify -from plexapi.exceptions import NotFound - -from homeassistant.components.plex.const import ( - CONF_SERVER, - CONF_SERVER_IDENTIFIER, - PLEX_SERVER_CONFIG, -) -from homeassistant.const import CONF_URL - -from .const import DEFAULT_DATA, MOCK_SERVERS, MOCK_USERS GDM_SERVER_PAYLOAD = [ { @@ -94,520 +81,3 @@ class MockGDM: self.entries = GDM_CLIENT_PAYLOAD else: self.entries = GDM_SERVER_PAYLOAD - - -class MockResource: - """Mock a PlexAccount resource.""" - - def __init__(self, index, kind="server"): - """Initialize the object.""" - if kind == "server": - self.name = MOCK_SERVERS[index][CONF_SERVER] - self.clientIdentifier = MOCK_SERVERS[index][ # pylint: disable=invalid-name - CONF_SERVER_IDENTIFIER - ] - self.provides = ["server"] - self.device = MockPlexServer(index) - else: - self.name = f"plex.tv Resource Player {index+10}" - self.clientIdentifier = f"client-{index+10}" - self.provides = ["player"] - self.device = MockPlexClient( - baseurl=f"http://192.168.0.1{index}:32500", index=index + 10 - ) - self.presence = index == 0 - self.publicAddressMatches = True - - def connect(self, timeout): - """Mock the resource connect method.""" - return self.device - - -class MockPlexAccount: - """Mock a PlexAccount instance.""" - - def __init__(self, servers=1, players=3): - """Initialize the object.""" - self._resources = [] - for index in range(servers): - self._resources.append(MockResource(index)) - for index in range(players): - self._resources.append(MockResource(index, kind="player")) - - def resource(self, name): - """Mock the PlexAccount resource lookup method.""" - return [x for x in self._resources if x.name == name][0] - - def resources(self): - """Mock the PlexAccount resources listing method.""" - return self._resources - - def sonos_speaker(self, speaker_name): - """Mock the PlexAccount Sonos lookup method.""" - return MockPlexSonosClient(speaker_name) - - -class MockPlexSystemAccount: - """Mock a PlexSystemAccount instance.""" - - def __init__(self, index): - """Initialize the object.""" - # Start accountIDs at 1 to set proper owner. - self.name = list(MOCK_USERS)[index] - self.accountID = index + 1 - - -class MockPlexServer: - """Mock a PlexServer instance.""" - - def __init__( - self, - index=0, - config_entry=None, - num_users=len(MOCK_USERS), - session_type="video", - ): - """Initialize the object.""" - if config_entry: - self._data = config_entry.data - else: - self._data = DEFAULT_DATA - - self._baseurl = self._data[PLEX_SERVER_CONFIG][CONF_URL] - self.friendlyName = self._data[CONF_SERVER] - self.machineIdentifier = self._data[CONF_SERVER_IDENTIFIER] - - self._systemAccounts = list(map(MockPlexSystemAccount, range(num_users))) - - self._clients = [] - self._session = None - self._sessions = [] - self.set_clients(num_users) - self.set_sessions(num_users, session_type) - - self._cache = {} - - def set_clients(self, num_clients): - """Set up mock PlexClients for this PlexServer.""" - self._clients = [ - MockPlexClient(baseurl=self._baseurl, index=x) for x in range(num_clients) - ] - - def set_sessions(self, num_sessions, session_type): - """Set up mock PlexSessions for this PlexServer.""" - self._sessions = [ - MockPlexSession(self._clients[x], mediatype=session_type, index=x) - for x in range(num_sessions) - ] - - def clear_clients(self): - """Clear all active PlexClients.""" - self._clients = [] - - def clear_sessions(self): - """Clear all active PlexSessions.""" - self._sessions = [] - - def clients(self): - """Mock the clients method.""" - return self._clients - - def createToken(self): - """Mock the createToken method.""" - return "temporary_token" - - def sessions(self): - """Mock the sessions method.""" - return self._sessions - - def systemAccounts(self): - """Mock the systemAccounts lookup method.""" - return self._systemAccounts - - def url(self, path, includeToken=False): - """Mock method to generate a server URL.""" - return f"{self._baseurl}{path}" - - @property - def accounts(self): - """Mock the accounts property.""" - return set(MOCK_USERS) - - @property - def version(self): - """Mock version of PlexServer.""" - return "1.0" - - @reify - def library(self): - """Mock library object of PlexServer.""" - return MockPlexLibrary(self) - - def playlist(self, playlist): - """Mock the playlist lookup method.""" - return MockPlexMediaItem(playlist, mediatype="playlist") - - @lru_cache() - def playlists(self): - """Mock the playlists lookup method with a lazy init.""" - return [ - MockPlexPlaylist( - self.library.section("Movies").all() - + self.library.section("TV Shows").all() - ), - MockPlexPlaylist(self.library.section("Music").all()), - ] - - def fetchItem(self, item): - """Mock the fetchItem method.""" - for section in self.library.sections(): - result = section.fetchItem(item) - if result: - return result - - -class MockPlexClient: - """Mock a PlexClient instance.""" - - def __init__(self, server=None, baseurl=None, token=None, index=0): - """Initialize the object.""" - self.machineIdentifier = f"client-{index+1}" - self._baseurl = baseurl - self._index = index - - def url(self, key): - """Mock the url method.""" - return f"{self._baseurl}{key}" - - @property - def device(self): - """Mock the device attribute.""" - return "DEVICE" - - @property - def platform(self): - """Mock the platform attribute.""" - return "PLATFORM" - - @property - def product(self): - """Mock the product attribute.""" - if self._index == 1: - return "Plex Web" - return "PRODUCT" - - @property - def protocolCapabilities(self): - """Mock the protocolCapabilities attribute.""" - return ["playback"] - - @property - def state(self): - """Mock the state attribute.""" - return "playing" - - @property - def title(self): - """Mock the title attribute.""" - return "TITLE" - - @property - def version(self): - """Mock the version attribute.""" - return "1.0" - - def proxyThroughServer(self, value=True, server=None): - """Mock the proxyThroughServer method.""" - pass - - def playMedia(self, item): - """Mock the playMedia method.""" - pass - - -class MockPlexSession: - """Mock a PlexServer.sessions() instance.""" - - def __init__(self, player, mediatype, index=0): - """Initialize the object.""" - self.TYPE = mediatype - self.usernames = [list(MOCK_USERS)[index]] - self.players = [player] - self._section = MockPlexLibrarySection("Movies") - self.sessionKey = index + 1 - - @property - def duration(self): - """Mock the duration attribute.""" - return 10000000 - - @property - def librarySectionID(self): - """Mock the librarySectionID attribute.""" - return 1 - - @property - def ratingKey(self): - """Mock the ratingKey attribute.""" - return 123 - - def section(self): - """Mock the section method.""" - return self._section - - @property - def summary(self): - """Mock the summary attribute.""" - return "SUMMARY" - - @property - def thumbUrl(self): - """Mock the thumbUrl attribute.""" - return "http://1.2.3.4/thumb" - - @property - def title(self): - """Mock the title attribute.""" - return "TITLE" - - @property - def type(self): - """Mock the type attribute.""" - return "movie" - - @property - def viewOffset(self): - """Mock the viewOffset attribute.""" - return 0 - - @property - def year(self): - """Mock the year attribute.""" - return 2020 - - -class MockPlexLibrary: - """Mock a Plex Library instance.""" - - def __init__(self, plex_server): - """Initialize the object.""" - self._plex_server = plex_server - self._sections = {} - - for kind in ["Movies", "Music", "TV Shows", "Photos"]: - self._sections[kind] = MockPlexLibrarySection(kind) - - def section(self, title): - """Mock the LibrarySection lookup.""" - section = self._sections.get(title) - if section: - return section - raise NotFound - - def sections(self): - """Return all available sections.""" - return self._sections.values() - - def sectionByID(self, section_id): - """Mock the sectionByID lookup.""" - return [x for x in self.sections() if x.key == section_id][0] - - def onDeck(self): - """Mock an empty On Deck folder.""" - return [] - - def recentlyAdded(self): - """Mock an empty Recently Added folder.""" - return [] - - -class MockPlexLibrarySection: - """Mock a Plex LibrarySection instance.""" - - def __init__(self, library): - """Initialize the object.""" - self.title = library - - if library == "Music": - self._item = MockPlexArtist("Artist") - elif library == "TV Shows": - self._item = MockPlexShow("TV Show") - else: - self._item = MockPlexMediaItem(library[:-1]) - - def get(self, query): - """Mock the get lookup method.""" - if self._item.title == query: - return self._item - raise NotFound - - def all(self): - """Mock the all method.""" - return [self._item] - - def fetchItem(self, ratingKey): - """Return a specific item.""" - for item in self.all(): - if item.ratingKey == ratingKey: - return item - if item._children: - for child in item._children: - if child.ratingKey == ratingKey: - return child - - def onDeck(self): - """Mock an empty On Deck folder.""" - return [] - - def recentlyAdded(self): - """Mock an empty Recently Added folder.""" - return self.all() - - @property - def type(self): - """Mock the library type.""" - if self.title == "Movies": - return "movie" - if self.title == "Music": - return "artist" - if self.title == "TV Shows": - return "show" - if self.title == "Photos": - return "photo" - - @property - def TYPE(self): - """Return the library type.""" - return self.type - - @property - def key(self): - """Mock the key identifier property.""" - return str(id(self.title)) - - def search(self, **kwargs): - """Mock the LibrarySection search method.""" - if kwargs.get("libtype") == "movie": - return self.all() - - def update(self): - """Mock the update call.""" - pass - - -class MockPlexMediaItem: - """Mock a Plex Media instance.""" - - def __init__(self, title, mediatype="video", year=2020): - """Initialize the object.""" - self.title = str(title) - self.type = mediatype - self.thumbUrl = "http://1.2.3.4/thumb.png" - self.year = year - self._children = [] - - def __iter__(self): - """Provide iterator.""" - yield from self._children - - @property - def ratingKey(self): - """Mock the ratingKey property.""" - return id(self.title) - - -class MockPlexPlaylist(MockPlexMediaItem): - """Mock a Plex Playlist instance.""" - - def __init__(self, items): - """Initialize the object.""" - super().__init__(f"Playlist ({len(items)} Items)", "playlist") - for item in items: - self._children.append(item) - - -class MockPlexShow(MockPlexMediaItem): - """Mock a Plex Show instance.""" - - def __init__(self, show): - """Initialize the object.""" - super().__init__(show, "show") - for index in range(1, 5): - self._children.append(MockPlexSeason(index)) - - def season(self, season): - """Mock the season lookup method.""" - return [x for x in self._children if x.title == f"Season {season}"][0] - - -class MockPlexSeason(MockPlexMediaItem): - """Mock a Plex Season instance.""" - - def __init__(self, season): - """Initialize the object.""" - super().__init__(f"Season {season}", "season") - for index in range(1, 10): - self._children.append(MockPlexMediaItem(f"Episode {index}", "episode")) - - def episode(self, episode): - """Mock the episode lookup method.""" - return self._children[episode - 1] - - -class MockPlexAlbum(MockPlexMediaItem): - """Mock a Plex Album instance.""" - - def __init__(self, album): - """Initialize the object.""" - super().__init__(album, "album") - for index in range(1, 10): - self._children.append(MockPlexMediaTrack(index)) - - def track(self, track): - """Mock the track lookup method.""" - try: - return [x for x in self._children if x.title == track][0] - except IndexError: - raise NotFound - - def tracks(self): - """Mock the tracks lookup method.""" - return self._children - - -class MockPlexArtist(MockPlexMediaItem): - """Mock a Plex Artist instance.""" - - def __init__(self, artist): - """Initialize the object.""" - super().__init__(artist, "artist") - self._album = MockPlexAlbum("Album") - - def album(self, album): - """Mock the album lookup method.""" - return self._album - - def get(self, track): - """Mock the track lookup method.""" - return self._album.track(track) - - -class MockPlexMediaTrack(MockPlexMediaItem): - """Mock a Plex Track instance.""" - - def __init__(self, index=1): - """Initialize the object.""" - super().__init__(f"Track {index}", "track") - self.index = index - - -class MockPlexSonosClient: - """Mock a PlexSonosClient instance.""" - - def __init__(self, name): - """Initialize the object.""" - self.name = name - - def playMedia(self, item): - """Mock the playMedia method.""" - pass diff --git a/tests/components/plex/test_browse_media.py b/tests/components/plex/test_browse_media.py index 66cbc51ef82..f9966a18c27 100644 --- a/tests/components/plex/test_browse_media.py +++ b/tests/components/plex/test_browse_media.py @@ -10,7 +10,7 @@ from homeassistant.components.websocket_api.const import ERR_UNKNOWN_ERROR, TYPE from .const import DEFAULT_DATA -async def test_browse_media(hass, hass_ws_client, mock_plex_server, mock_websocket): +async def test_browse_media(hass, hass_ws_client, mock_plex_server, requests_mock): """Test getting Plex clients from plex.tv.""" websocket_client = await hass_ws_client(hass) @@ -51,8 +51,10 @@ async def test_browse_media(hass, hass_ws_client, mock_plex_server, mock_websock result = msg["result"] assert result[ATTR_MEDIA_CONTENT_TYPE] == "server" assert result[ATTR_MEDIA_CONTENT_ID] == DEFAULT_DATA[CONF_SERVER_IDENTIFIER] - assert len(result["children"]) == len(mock_plex_server.library.sections()) + len( - SPECIAL_METHODS + # Library Sections + Special Sections + Playlists + assert ( + len(result["children"]) + == len(mock_plex_server.library.sections()) + len(SPECIAL_METHODS) + 1 ) tvshows = next(iter(x for x in result["children"] if x["title"] == "TV Shows")) @@ -149,9 +151,14 @@ async def test_browse_media(hass, hass_ws_client, mock_plex_server, mock_websock result = msg["result"] assert result[ATTR_MEDIA_CONTENT_TYPE] == "show" result_id = int(result[ATTR_MEDIA_CONTENT_ID]) - assert result["title"] == mock_plex_server.fetchItem(result_id).title + assert result["title"] == mock_plex_server.fetch_item(result_id).title # Browse into a non-existent TV season + unknown_key = 99999999999999 + requests_mock.get( + f"{mock_plex_server.url_in_use}/library/metadata/{unknown_key}", status_code=404 + ) + msg_id += 1 await websocket_client.send_json( { @@ -159,7 +166,7 @@ async def test_browse_media(hass, hass_ws_client, mock_plex_server, mock_websock "type": "media_player/browse_media", "entity_id": media_players[0], ATTR_MEDIA_CONTENT_TYPE: result["children"][0][ATTR_MEDIA_CONTENT_TYPE], - ATTR_MEDIA_CONTENT_ID: str(99999999999999), + ATTR_MEDIA_CONTENT_ID: str(unknown_key), } ) diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index d8c010ceb92..bc0e59e658f 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -1,6 +1,7 @@ """Tests for Plex config flow.""" import copy import ssl +from unittest.mock import patch import plexapi.exceptions import requests.exceptions @@ -34,18 +35,12 @@ from homeassistant.const import ( CONF_URL, CONF_VERIFY_SSL, ) +from homeassistant.setup import async_setup_component -from .const import DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN +from .const import DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN, PLEX_DIRECT_URL from .helpers import trigger_plex_update, wait_for_debouncer -from .mock_classes import ( - MockGDM, - MockPlexAccount, - MockPlexClient, - MockPlexServer, - MockResource, -) +from .mock_classes import MockGDM -from tests.async_mock import patch from tests.common import MockConfigEntry @@ -82,7 +77,7 @@ async def test_bad_credentials(hass): assert result["errors"][CONF_TOKEN] == "faulty_credentials" -async def test_bad_hostname(hass): +async def test_bad_hostname(hass, mock_plex_calls): """Test when an invalid address is provided.""" await async_process_ha_core_config( hass, @@ -96,12 +91,9 @@ async def test_bad_hostname(hass): assert result["step_id"] == "user" with patch( - "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() - ), patch.object( - MockResource, "connect", side_effect=requests.exceptions.ConnectionError - ), patch( - "plexauth.PlexAuth.initiate_auth" - ), patch( + "plexapi.myplex.MyPlexResource.connect", + side_effect=requests.exceptions.ConnectionError, + ), patch("plexauth.PlexAuth.initiate_auth"), patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): result = await hass.config_entries.flow.async_configure( @@ -148,8 +140,9 @@ async def test_unknown_exception(hass): assert result["reason"] == "unknown" -async def test_no_servers_found(hass): +async def test_no_servers_found(hass, mock_plex_calls, requests_mock, empty_payload): """Test when no servers are on an account.""" + requests_mock.get("https://plex.tv/api/resources", text=empty_payload) await async_process_ha_core_config( hass, @@ -162,9 +155,7 @@ async def test_no_servers_found(hass): assert result["type"] == "form" assert result["step_id"] == "user" - with patch( - "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=0) - ), patch("plexauth.PlexAuth.initiate_auth"), patch( + with patch("plexauth.PlexAuth.initiate_auth"), patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): result = await hass.config_entries.flow.async_configure( @@ -181,11 +172,9 @@ async def test_no_servers_found(hass): assert result["errors"]["base"] == "no_servers" -async def test_single_available_server(hass): +async def test_single_available_server(hass, mock_plex_calls): """Test creating an entry with one server available.""" - mock_plex_server = MockPlexServer() - await async_process_ha_core_config( hass, {"internal_url": "http://example.local:8123"}, @@ -197,9 +186,7 @@ async def test_single_available_server(hass): assert result["type"] == "form" assert result["step_id"] == "user" - with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()), patch( - "plexapi.server.PlexServer", return_value=mock_plex_server - ), patch("plexauth.PlexAuth.initiate_auth"), patch( + with patch("plexauth.PlexAuth.initiate_auth"), patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): result = await hass.config_entries.flow.async_configure( @@ -212,20 +199,27 @@ async def test_single_available_server(hass): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "create_entry" - assert result["title"] == mock_plex_server.friendlyName - assert result["data"][CONF_SERVER] == mock_plex_server.friendlyName + + server_id = result["data"][CONF_SERVER_IDENTIFIER] + mock_plex_server = hass.data[DOMAIN][SERVERS][server_id] + + assert result["title"] == mock_plex_server.friendly_name + assert result["data"][CONF_SERVER] == mock_plex_server.friendly_name assert ( - result["data"][CONF_SERVER_IDENTIFIER] == mock_plex_server.machineIdentifier + result["data"][CONF_SERVER_IDENTIFIER] + == mock_plex_server.machine_identifier + ) + assert ( + result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server.url_in_use ) - assert result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server._baseurl assert result["data"][PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN -async def test_multiple_servers_with_selection(hass): +async def test_multiple_servers_with_selection( + hass, mock_plex_calls, requests_mock, plextv_resources_base +): """Test creating an entry with multiple servers available.""" - mock_plex_server = MockPlexServer() - await async_process_ha_core_config( hass, {"internal_url": "http://example.local:8123"}, @@ -237,11 +231,11 @@ async def test_multiple_servers_with_selection(hass): assert result["type"] == "form" assert result["step_id"] == "user" - with patch( - "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2) - ), patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "plexauth.PlexAuth.initiate_auth" - ), patch( + requests_mock.get( + "https://plex.tv/api/resources", + text=plextv_resources_base.format(second_server_enabled=1), + ) + with patch("plexauth.PlexAuth.initiate_auth"), patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): result = await hass.config_entries.flow.async_configure( @@ -261,20 +255,27 @@ async def test_multiple_servers_with_selection(hass): user_input={CONF_SERVER: MOCK_SERVERS[0][CONF_SERVER]}, ) assert result["type"] == "create_entry" - assert result["title"] == mock_plex_server.friendlyName - assert result["data"][CONF_SERVER] == mock_plex_server.friendlyName + + server_id = result["data"][CONF_SERVER_IDENTIFIER] + mock_plex_server = hass.data[DOMAIN][SERVERS][server_id] + + assert result["title"] == mock_plex_server.friendly_name + assert result["data"][CONF_SERVER] == mock_plex_server.friendly_name assert ( - result["data"][CONF_SERVER_IDENTIFIER] == mock_plex_server.machineIdentifier + result["data"][CONF_SERVER_IDENTIFIER] + == mock_plex_server.machine_identifier + ) + assert ( + result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server.url_in_use ) - assert result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server._baseurl assert result["data"][PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN -async def test_adding_last_unconfigured_server(hass): +async def test_adding_last_unconfigured_server( + hass, mock_plex_calls, requests_mock, plextv_resources_base +): """Test automatically adding last unconfigured server when multiple servers on account.""" - mock_plex_server = MockPlexServer() - await async_process_ha_core_config( hass, {"internal_url": "http://example.local:8123"}, @@ -294,11 +295,12 @@ async def test_adding_last_unconfigured_server(hass): assert result["type"] == "form" assert result["step_id"] == "user" - with patch( - "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2) - ), patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "plexauth.PlexAuth.initiate_auth" - ), patch( + requests_mock.get( + "https://plex.tv/api/resources", + text=plextv_resources_base.format(second_server_enabled=1), + ) + + with patch("plexauth.PlexAuth.initiate_auth"), patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): result = await hass.config_entries.flow.async_configure( @@ -311,16 +313,25 @@ async def test_adding_last_unconfigured_server(hass): result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "create_entry" - assert result["title"] == mock_plex_server.friendlyName - assert result["data"][CONF_SERVER] == mock_plex_server.friendlyName + + server_id = result["data"][CONF_SERVER_IDENTIFIER] + mock_plex_server = hass.data[DOMAIN][SERVERS][server_id] + + assert result["title"] == mock_plex_server.friendly_name + assert result["data"][CONF_SERVER] == mock_plex_server.friendly_name assert ( - result["data"][CONF_SERVER_IDENTIFIER] == mock_plex_server.machineIdentifier + result["data"][CONF_SERVER_IDENTIFIER] + == mock_plex_server.machine_identifier + ) + assert ( + result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server.url_in_use ) - assert result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server._baseurl assert result["data"][PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN -async def test_all_available_servers_configured(hass): +async def test_all_available_servers_configured( + hass, entry, requests_mock, plextv_account, plextv_resources_base +): """Test when all available servers are already configured.""" await async_process_ha_core_config( @@ -328,13 +339,7 @@ async def test_all_available_servers_configured(hass): {"internal_url": "http://example.local:8123"}, ) - MockConfigEntry( - domain=DOMAIN, - data={ - CONF_SERVER_IDENTIFIER: MOCK_SERVERS[0][CONF_SERVER_IDENTIFIER], - CONF_SERVER: MOCK_SERVERS[0][CONF_SERVER], - }, - ).add_to_hass(hass) + entry.add_to_hass(hass) MockConfigEntry( domain=DOMAIN, @@ -350,9 +355,13 @@ async def test_all_available_servers_configured(hass): assert result["type"] == "form" assert result["step_id"] == "user" - with patch( - "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=2) - ), patch("plexauth.PlexAuth.initiate_auth"), patch( + requests_mock.get("https://plex.tv/users/account", text=plextv_account) + requests_mock.get( + "https://plex.tv/api/resources", + text=plextv_resources_base.format(second_server_enabled=1), + ) + + with patch("plexauth.PlexAuth.initiate_auth"), patch( "plexauth.PlexAuth.token", return_value=MOCK_TOKEN ): result = await hass.config_entries.flow.async_configure( @@ -432,32 +441,22 @@ async def test_missing_option_flow(hass, entry, mock_plex_server): } -async def test_option_flow_new_users_available( - hass, caplog, entry, mock_websocket, setup_plex_server -): +async def test_option_flow_new_users_available(hass, entry, setup_plex_server): """Test config options multiselect defaults when new Plex users are seen.""" OPTIONS_OWNER_ONLY = copy.deepcopy(DEFAULT_OPTIONS) - OPTIONS_OWNER_ONLY[MP_DOMAIN][CONF_MONITORED_USERS] = {"Owner": {"enabled": True}} + OPTIONS_OWNER_ONLY[MP_DOMAIN][CONF_MONITORED_USERS] = {"User 1": {"enabled": True}} entry.options = OPTIONS_OWNER_ONLY - with patch("homeassistant.components.plex.server.PlexClient", new=MockPlexClient): - mock_plex_server = await setup_plex_server( - config_entry=entry, disable_gdm=False - ) - await hass.async_block_till_done() + mock_plex_server = await setup_plex_server(config_entry=entry) + await hass.async_block_till_done() - server_id = mock_plex_server.machineIdentifier + server_id = mock_plex_server.machine_identifier monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users new_users = [x for x in mock_plex_server.accounts if x not in monitored_users] assert len(monitored_users) == 1 assert len(new_users) == 2 - await wait_for_debouncer(hass) - - sensor = hass.states.get("sensor.plex_plex_server_1") - assert sensor.state == str(len(mock_plex_server.accounts)) - result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"}, data=None ) @@ -465,7 +464,7 @@ async def test_option_flow_new_users_available( assert result["step_id"] == "plex_mp_settings" multiselect_defaults = result["data_schema"].schema["monitored_users"].options - assert "[Owner]" in multiselect_defaults["Owner"] + assert "[Owner]" in multiselect_defaults["User 1"] for user in new_users: assert "[New]" in multiselect_defaults[user] @@ -529,7 +528,7 @@ async def test_callback_view(hass, aiohttp_client): assert resp.status == 200 -async def test_manual_config(hass): +async def test_manual_config(hass, mock_plex_calls): """Test creating via manual configuration.""" await async_process_ha_core_config( hass, @@ -587,8 +586,6 @@ async def test_manual_config(hass): assert result["type"] == "form" assert result["step_id"] == "manual_setup" - mock_plex_server = MockPlexServer() - MANUAL_SERVER = { CONF_HOST: MOCK_SERVERS[0][CONF_HOST], CONF_PORT: MOCK_SERVERS[0][CONF_PORT], @@ -647,26 +644,26 @@ async def test_manual_config(hass): assert result["step_id"] == "manual_setup" assert result["errors"]["base"] == "ssl_error" - with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( - "homeassistant.components.plex.PlexWebsocket", autospec=True - ), patch( + with patch("homeassistant.components.plex.PlexWebsocket", autospec=True), patch( "homeassistant.components.plex.GDM", return_value=MockGDM(disabled=True) - ), patch( - "plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount() ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MANUAL_SERVER ) assert result["type"] == "create_entry" - assert result["title"] == mock_plex_server.friendlyName - assert result["data"][CONF_SERVER] == mock_plex_server.friendlyName - assert result["data"][CONF_SERVER_IDENTIFIER] == mock_plex_server.machineIdentifier - assert result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server._baseurl + + server_id = result["data"][CONF_SERVER_IDENTIFIER] + mock_plex_server = hass.data[DOMAIN][SERVERS][server_id] + + assert result["title"] == mock_plex_server.friendly_name + assert result["data"][CONF_SERVER] == mock_plex_server.friendly_name + assert result["data"][CONF_SERVER_IDENTIFIER] == mock_plex_server.machine_identifier + assert result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server.url_in_use assert result["data"][PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN -async def test_manual_config_with_token(hass): +async def test_manual_config_with_token(hass, mock_plex_calls): """Test creating via manual configuration with only token.""" result = await hass.config_entries.flow.async_init( @@ -683,37 +680,36 @@ async def test_manual_config_with_token(hass): assert result["type"] == "form" assert result["step_id"] == "manual_setup" - mock_plex_server = MockPlexServer() - - with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()), patch( - "plexapi.server.PlexServer", return_value=mock_plex_server - ), patch( + with patch( "homeassistant.components.plex.GDM", return_value=MockGDM(disabled=True) - ), patch( - "homeassistant.components.plex.PlexWebsocket", autospec=True - ): + ), patch("homeassistant.components.plex.PlexWebsocket", autospec=True): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_TOKEN: MOCK_TOKEN} ) assert result["type"] == "create_entry" - assert result["title"] == mock_plex_server.friendlyName - assert result["data"][CONF_SERVER] == mock_plex_server.friendlyName - assert result["data"][CONF_SERVER_IDENTIFIER] == mock_plex_server.machineIdentifier - assert result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server._baseurl + + server_id = result["data"][CONF_SERVER_IDENTIFIER] + mock_plex_server = hass.data[DOMAIN][SERVERS][server_id] + + assert result["title"] == mock_plex_server.friendly_name + assert result["data"][CONF_SERVER] == mock_plex_server.friendly_name + assert result["data"][CONF_SERVER_IDENTIFIER] == mock_plex_server.machine_identifier + assert result["data"][PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server.url_in_use assert result["data"][PLEX_SERVER_CONFIG][CONF_TOKEN] == MOCK_TOKEN async def test_setup_with_limited_credentials(hass, entry, setup_plex_server): """Test setup with a user with limited permissions.""" - with patch.object( - MockPlexServer, "systemAccounts", side_effect=plexapi.exceptions.Unauthorized + with patch( + "plexapi.server.PlexServer.systemAccounts", + side_effect=plexapi.exceptions.Unauthorized, ) as mock_accounts: mock_plex_server = await setup_plex_server() assert mock_accounts.called - plex_server = hass.data[DOMAIN][SERVERS][mock_plex_server.machineIdentifier] + plex_server = hass.data[DOMAIN][SERVERS][mock_plex_server.machine_identifier] assert len(plex_server.accounts) == 0 assert plex_server.owner is None @@ -745,6 +741,7 @@ async def test_integration_discovery(hass): async def test_trigger_reauth(hass, entry, mock_plex_server, mock_websocket): """Test setup and reauthorization of a Plex token.""" + await async_setup_component(hass, "persistent_notification", {}) await async_process_ha_core_config( hass, {"internal_url": "http://example.local:8123"}, @@ -752,8 +749,8 @@ async def test_trigger_reauth(hass, entry, mock_plex_server, mock_websocket): assert entry.state == ENTRY_STATE_LOADED - with patch.object( - mock_plex_server, "clients", side_effect=plexapi.exceptions.Unauthorized + with patch( + "plexapi.server.PlexServer.clients", side_effect=plexapi.exceptions.Unauthorized ), patch("plexapi.server.PlexServer", side_effect=plexapi.exceptions.Unauthorized): trigger_plex_update(mock_websocket) await wait_for_debouncer(hass) @@ -767,9 +764,7 @@ async def test_trigger_reauth(hass, entry, mock_plex_server, mock_websocket): flow_id = flows[0]["flow_id"] - with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()), patch( - "plexapi.server.PlexServer", return_value=mock_plex_server - ), patch("plexauth.PlexAuth.initiate_auth"), patch( + with patch("plexauth.PlexAuth.initiate_auth"), patch( "plexauth.PlexAuth.token", return_value="BRAND_NEW_TOKEN" ): result = await hass.config_entries.flow.async_configure(flow_id, user_input={}) @@ -787,7 +782,7 @@ async def test_trigger_reauth(hass, entry, mock_plex_server, mock_websocket): assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert entry.state == ENTRY_STATE_LOADED - assert entry.data[CONF_SERVER] == mock_plex_server.friendlyName - assert entry.data[CONF_SERVER_IDENTIFIER] == mock_plex_server.machineIdentifier - assert entry.data[PLEX_SERVER_CONFIG][CONF_URL] == mock_plex_server._baseurl + assert entry.data[CONF_SERVER] == mock_plex_server.friendly_name + assert entry.data[CONF_SERVER_IDENTIFIER] == mock_plex_server.machine_identifier + assert entry.data[PLEX_SERVER_CONFIG][CONF_URL] == PLEX_DIRECT_URL assert entry.data[PLEX_SERVER_CONFIG][CONF_TOKEN] == "BRAND_NEW_TOKEN" diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index a1a159010ef..95d2ef9bddb 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -2,6 +2,7 @@ import copy from datetime import timedelta import ssl +from unittest.mock import patch import plexapi import requests @@ -14,13 +15,12 @@ from homeassistant.config_entries import ( ENTRY_STATE_SETUP_RETRY, ) from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL, STATE_IDLE +from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from .const import DEFAULT_DATA, DEFAULT_OPTIONS +from .const import DEFAULT_DATA, DEFAULT_OPTIONS, PLEX_DIRECT_URL from .helpers import trigger_plex_update, wait_for_debouncer -from .mock_classes import MockGDM, MockPlexAccount, MockPlexServer -from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed @@ -31,7 +31,7 @@ async def test_set_config_entry_unique_id(hass, entry, mock_plex_server): assert ( hass.config_entries.async_entries(const.DOMAIN)[0].unique_id - == mock_plex_server.machineIdentifier + == mock_plex_server.machine_identifier ) @@ -79,9 +79,9 @@ async def test_unload_config_entry(hass, entry, mock_plex_server): assert entry is config_entries[0] assert entry.state == ENTRY_STATE_LOADED - server_id = mock_plex_server.machineIdentifier + server_id = mock_plex_server.machine_identifier loaded_server = hass.data[const.DOMAIN][const.SERVERS][server_id] - assert loaded_server.plex_server == mock_plex_server + assert loaded_server == mock_plex_server websocket = hass.data[const.DOMAIN][const.WEBSOCKETS][server_id] await hass.config_entries.async_unload(entry.entry_id) @@ -89,7 +89,7 @@ async def test_unload_config_entry(hass, entry, mock_plex_server): assert entry.state == ENTRY_STATE_NOT_LOADED -async def test_setup_with_photo_session(hass, entry, mock_websocket, setup_plex_server): +async def test_setup_with_photo_session(hass, entry, setup_plex_server): """Test setup component with config.""" await setup_plex_server(session_type="photo") @@ -97,7 +97,9 @@ async def test_setup_with_photo_session(hass, entry, mock_websocket, setup_plex_ assert entry.state == ENTRY_STATE_LOADED await hass.async_block_till_done() - media_player = hass.states.get("media_player.plex_product_title") + media_player = hass.states.get( + "media_player.plex_plex_for_android_tv_shield_android_tv" + ) assert media_player.state == STATE_IDLE await wait_for_debouncer(hass) @@ -106,14 +108,17 @@ async def test_setup_with_photo_session(hass, entry, mock_websocket, setup_plex_ assert sensor.state == "0" -async def test_setup_when_certificate_changed(hass, entry): +async def test_setup_when_certificate_changed( + hass, + requests_mock, + empty_payload, + plex_server_accounts, + plex_server_default, + plextv_account, + plextv_resources, +): """Test setup component when the Plex certificate has changed.""" - - old_domain = "1-2-3-4.1234567890abcdef1234567890abcdef.plex.direct" - old_url = f"https://{old_domain}:32400" - - OLD_HOSTNAME_DATA = copy.deepcopy(DEFAULT_DATA) - OLD_HOSTNAME_DATA[const.PLEX_SERVER_CONFIG][CONF_URL] = old_url + await async_setup_component(hass, "persistent_notification", {}) class WrongCertHostnameException(requests.exceptions.SSLError): """Mock the exception showing a mismatched hostname.""" @@ -123,6 +128,12 @@ async def test_setup_when_certificate_changed(hass, entry): f"hostname '{old_domain}' doesn't match" ) + old_domain = "1-2-3-4.1111111111ffffff1111111111ffffff.plex.direct" + old_url = f"https://{old_domain}:32400" + + OLD_HOSTNAME_DATA = copy.deepcopy(DEFAULT_DATA) + OLD_HOSTNAME_DATA[const.PLEX_SERVER_CONFIG][CONF_URL] = old_url + old_entry = MockConfigEntry( domain=const.DOMAIN, data=OLD_HOSTNAME_DATA, @@ -130,46 +141,45 @@ async def test_setup_when_certificate_changed(hass, entry): unique_id=DEFAULT_DATA["server_id"], ) + requests_mock.get("https://plex.tv/users/account", text=plextv_account) + requests_mock.get("https://plex.tv/api/resources", text=plextv_resources) + requests_mock.get(old_url, exc=WrongCertHostnameException) + # Test with account failure - with patch( - "plexapi.server.PlexServer", side_effect=WrongCertHostnameException - ), patch( - "plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized - ): - old_entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(old_entry.entry_id) is False - await hass.async_block_till_done() + requests_mock.get(f"{old_url}/accounts", status_code=401) + old_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(old_entry.entry_id) is False + await hass.async_block_till_done() assert old_entry.state == ENTRY_STATE_SETUP_ERROR await hass.config_entries.async_unload(old_entry.entry_id) # Test with no servers found - with patch( - "plexapi.server.PlexServer", side_effect=WrongCertHostnameException - ), patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(servers=0)): - assert await hass.config_entries.async_setup(old_entry.entry_id) is False - await hass.async_block_till_done() + requests_mock.get(f"{old_url}/accounts", text=plex_server_accounts) + requests_mock.get("https://plex.tv/api/resources", text=empty_payload) + + assert await hass.config_entries.async_setup(old_entry.entry_id) is False + await hass.async_block_till_done() assert old_entry.state == ENTRY_STATE_SETUP_ERROR await hass.config_entries.async_unload(old_entry.entry_id) # Test with success - with patch( - "plexapi.server.PlexServer", side_effect=WrongCertHostnameException - ), patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()): - assert await hass.config_entries.async_setup(old_entry.entry_id) - await hass.async_block_till_done() + new_url = PLEX_DIRECT_URL + requests_mock.get("https://plex.tv/api/resources", text=plextv_resources) + requests_mock.get(new_url, text=plex_server_default) + requests_mock.get(f"{new_url}/accounts", text=plex_server_accounts) + + assert await hass.config_entries.async_setup(old_entry.entry_id) + await hass.async_block_till_done() assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 assert old_entry.state == ENTRY_STATE_LOADED - assert ( - old_entry.data[const.PLEX_SERVER_CONFIG][CONF_URL] - == entry.data[const.PLEX_SERVER_CONFIG][CONF_URL] - ) + assert old_entry.data[const.PLEX_SERVER_CONFIG][CONF_URL] == new_url -async def test_tokenless_server(hass, entry, mock_websocket, setup_plex_server): +async def test_tokenless_server(entry, setup_plex_server): """Test setup with a server with token auth disabled.""" TOKENLESS_DATA = copy.deepcopy(DEFAULT_DATA) TOKENLESS_DATA[const.PLEX_SERVER_CONFIG].pop(CONF_TOKEN, None) @@ -179,18 +189,13 @@ async def test_tokenless_server(hass, entry, mock_websocket, setup_plex_server): assert entry.state == ENTRY_STATE_LOADED -async def test_bad_token_with_tokenless_server(hass, entry): +async def test_bad_token_with_tokenless_server( + hass, entry, mock_websocket, setup_plex_server, requests_mock +): """Test setup with a bad token and a server with token auth disabled.""" - with patch("plexapi.server.PlexServer", return_value=MockPlexServer()), patch( - "plexapi.myplex.MyPlexAccount", side_effect=plexapi.exceptions.Unauthorized - ), patch( - "homeassistant.components.plex.GDM", return_value=MockGDM(disabled=True) - ), patch( - "homeassistant.components.plex.PlexWebsocket", autospec=True - ) as mock_websocket: - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + requests_mock.get("https://plex.tv/users/account", status_code=401) + + await setup_plex_server() assert entry.state == ENTRY_STATE_LOADED diff --git a/tests/components/plex/test_media_players.py b/tests/components/plex/test_media_players.py index a4bda5467e2..092d7e09008 100644 --- a/tests/components/plex/test_media_players.py +++ b/tests/components/plex/test_media_players.py @@ -1,41 +1,31 @@ """Tests for Plex media_players.""" +from unittest.mock import patch + from plexapi.exceptions import NotFound -from homeassistant.components.plex.const import DOMAIN, SERVERS -from tests.async_mock import patch - - -async def test_plex_tv_clients(hass, entry, mock_plex_account, setup_plex_server): +async def test_plex_tv_clients( + hass, entry, setup_plex_server, requests_mock, player_plexweb_resources +): """Test getting Plex clients from plex.tv.""" - resource = next( - x - for x in mock_plex_account.resources() - if x.name.startswith("plex.tv Resource Player") - ) - with patch.object(resource, "connect", side_effect=NotFound): - mock_plex_server = await setup_plex_server() + requests_mock.get("/resources", text=player_plexweb_resources) + + with patch("plexapi.myplex.MyPlexResource.connect", side_effect=NotFound): + await setup_plex_server() await hass.async_block_till_done() - server_id = mock_plex_server.machineIdentifier - plex_server = hass.data[DOMAIN][SERVERS][server_id] media_players_before = len(hass.states.async_entity_ids("media_player")) + await hass.config_entries.async_unload(entry.entry_id) # Ensure one more client is discovered - await hass.config_entries.async_unload(entry.entry_id) - mock_plex_server = await setup_plex_server() - plex_server = hass.data[DOMAIN][SERVERS][server_id] + await setup_plex_server() media_players_after = len(hass.states.async_entity_ids("media_player")) assert media_players_after == media_players_before + 1 - # Ensure only plex.tv resource client is found await hass.config_entries.async_unload(entry.entry_id) - mock_plex_server = await setup_plex_server(num_users=0) - plex_server = hass.data[DOMAIN][SERVERS][server_id] - assert len(hass.states.async_entity_ids("media_player")) == 1 - # Ensure cache gets called - await plex_server._async_update_platforms() - await hass.async_block_till_done() + # Ensure only plex.tv resource client is found + with patch("plexapi.server.PlexServer.sessions", return_value=[]): + await setup_plex_server(disable_clients=True) assert len(hass.states.async_entity_ids("media_player")) == 1 diff --git a/tests/components/plex/test_playback.py b/tests/components/plex/test_playback.py index 5119286d3b8..86e55dab613 100644 --- a/tests/components/plex/test_playback.py +++ b/tests/components/plex/test_playback.py @@ -1,160 +1,136 @@ """Tests for Plex player playback methods/services.""" -from plexapi.exceptions import NotFound +from unittest.mock import patch from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, + DOMAIN as MP_DOMAIN, + MEDIA_TYPE_EPISODE, + MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, -) -from homeassistant.components.plex.const import ( - CONF_SERVER, - DOMAIN, - SERVERS, - SERVICE_PLAY_ON_SONOS, + SERVICE_PLAY_MEDIA, ) from homeassistant.const import ATTR_ENTITY_ID -from homeassistant.exceptions import HomeAssistantError - -from .const import DEFAULT_OPTIONS, SECONDARY_DATA - -from tests.async_mock import patch -from tests.common import MockConfigEntry -async def test_sonos_playback(hass, mock_plex_server): - """Test playing media on a Sonos speaker.""" - server_id = mock_plex_server.machineIdentifier - loaded_server = hass.data[DOMAIN][SERVERS][server_id] - - # Test Sonos integration lookup failure - with patch.object( - hass.components.sonos, "get_coordinator_name", side_effect=HomeAssistantError - ): - assert await hass.services.async_call( - DOMAIN, - SERVICE_PLAY_ON_SONOS, - { - ATTR_ENTITY_ID: "media_player.sonos_kitchen", - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album"}', - }, - True, - ) - - # Test success with plex_key - with patch.object( - hass.components.sonos, - "get_coordinator_name", - return_value="media_player.sonos_kitchen", - ), patch("plexapi.playqueue.PlayQueue.create"): - assert await hass.services.async_call( - DOMAIN, - SERVICE_PLAY_ON_SONOS, - { - ATTR_ENTITY_ID: "media_player.sonos_kitchen", - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: "2", - }, - True, - ) - - # Test success with dict - with patch.object( - hass.components.sonos, - "get_coordinator_name", - return_value="media_player.sonos_kitchen", - ), patch("plexapi.playqueue.PlayQueue.create"): - assert await hass.services.async_call( - DOMAIN, - SERVICE_PLAY_ON_SONOS, - { - ATTR_ENTITY_ID: "media_player.sonos_kitchen", - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album"}', - }, - True, - ) - - # Test media lookup failure - with patch.object( - hass.components.sonos, - "get_coordinator_name", - return_value="media_player.sonos_kitchen", - ), patch.object(mock_plex_server, "fetchItem", side_effect=NotFound): - assert await hass.services.async_call( - DOMAIN, - SERVICE_PLAY_ON_SONOS, - { - ATTR_ENTITY_ID: "media_player.sonos_kitchen", - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: "999", - }, - True, - ) - - # Test invalid Plex server requested - with patch.object( - hass.components.sonos, - "get_coordinator_name", - return_value="media_player.sonos_kitchen", - ): - assert await hass.services.async_call( - DOMAIN, - SERVICE_PLAY_ON_SONOS, - { - ATTR_ENTITY_ID: "media_player.sonos_kitchen", - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: '{"plex_server": "unknown_plex_server", "library_name": "Music", "artist_name": "Artist", "album_name": "Album"}', - }, - True, - ) - - # Test no speakers available - with patch.object( - loaded_server.account, "sonos_speaker", return_value=None - ), patch.object( - hass.components.sonos, - "get_coordinator_name", - return_value="media_player.sonos_kitchen", - ), patch( - "plexapi.playqueue.PlayQueue.create" - ): - assert await hass.services.async_call( - DOMAIN, - SERVICE_PLAY_ON_SONOS, - { - ATTR_ENTITY_ID: "media_player.sonos_kitchen", - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album"}', - }, - True, - ) - - -async def test_playback_multiple_servers(hass, mock_websocket, setup_plex_server): - """Test playing media when multiple servers available.""" - secondary_entry = MockConfigEntry( - domain=DOMAIN, - data=SECONDARY_DATA, - options=DEFAULT_OPTIONS, - unique_id=SECONDARY_DATA["server_id"], - ) +async def test_media_player_playback( + hass, setup_plex_server, requests_mock, playqueue_created, player_plexweb_resources +): + """Test playing media on a Plex media_player.""" + requests_mock.get("http://1.2.3.5:32400/resources", text=player_plexweb_resources) await setup_plex_server() - await setup_plex_server(config_entry=secondary_entry) - with patch.object( - hass.components.sonos, - "get_coordinator_name", - return_value="media_player.sonos_kitchen", - ), patch("plexapi.playqueue.PlayQueue.create"): + media_player = "media_player.plex_plex_web_chrome" + requests_mock.post("/playqueues", text=playqueue_created) + requests_mock.get("/player/playback/playMedia", status_code=200) + + # Test movie success + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "title": "Movie 1" }', + }, + True, + ) + + # Test movie incomplete dict + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies"}', + }, + True, + ) + + # Test movie failure with options + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "title": "Does not exist" }', + }, + True, + ) + + # Test movie failure with nothing found + with patch("plexapi.library.LibrarySection.search", return_value=None): assert await hass.services.async_call( - DOMAIN, - SERVICE_PLAY_ON_SONOS, + MP_DOMAIN, + SERVICE_PLAY_MEDIA, { - ATTR_ENTITY_ID: "media_player.sonos_kitchen", - ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, - ATTR_MEDIA_CONTENT_ID: f'{{"plex_server": "{SECONDARY_DATA[CONF_SERVER]}", "library_name": "Music", "artist_name": "Artist", "album_name": "Album"}}', + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MOVIE, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Movies", "title": "Does not exist" }', }, True, ) + + # Test movie success with dict + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album"}', + }, + True, + ) + + # Test TV show episoe lookup failure + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_EPISODE, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "TV Shows", "show_name": "TV Show", "season_number": 1, "episode_number": 99}', + }, + True, + ) + + # Test track name lookup failure + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album", "track_name": "Not a track"}', + }, + True, + ) + + # Test media lookup failure by key + requests_mock.get("/library/metadata/999", status_code=404) + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: "999", + }, + True, + ) + + # Test invalid Plex server requested + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: '{"plex_server": "unknown_plex_server", "library_name": "Music", "artist_name": "Artist", "album_name": "Album"}', + }, + True, + ) diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py index 99a324786f6..f9b34088601 100644 --- a/tests/components/plex/test_server.py +++ b/tests/components/plex/test_server.py @@ -1,5 +1,6 @@ """Tests for Plex server.""" import copy +from unittest.mock import patch from plexapi.exceptions import BadRequest, NotFound from requests.exceptions import ConnectionError, RequestException @@ -27,31 +28,18 @@ from homeassistant.const import ATTR_ENTITY_ID from .const import DEFAULT_DATA, DEFAULT_OPTIONS from .helpers import trigger_plex_update, wait_for_debouncer -from .mock_classes import ( - MockPlexAccount, - MockPlexAlbum, - MockPlexArtist, - MockPlexLibrary, - MockPlexLibrarySection, - MockPlexMediaItem, - MockPlexSeason, - MockPlexServer, - MockPlexShow, -) - -from tests.async_mock import patch -async def test_new_users_available(hass, entry, mock_websocket, setup_plex_server): +async def test_new_users_available(hass, entry, setup_plex_server): """Test setting up when new users available on Plex server.""" - MONITORED_USERS = {"Owner": {"enabled": True}} + MONITORED_USERS = {"User 1": {"enabled": True}} OPTIONS_WITH_USERS = copy.deepcopy(DEFAULT_OPTIONS) OPTIONS_WITH_USERS[MP_DOMAIN][CONF_MONITORED_USERS] = MONITORED_USERS entry.options = OPTIONS_WITH_USERS mock_plex_server = await setup_plex_server(config_entry=entry) - server_id = mock_plex_server.machineIdentifier + server_id = mock_plex_server.machine_identifier monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users @@ -59,17 +47,18 @@ async def test_new_users_available(hass, entry, mock_websocket, setup_plex_serve assert len(monitored_users) == 1 assert len(ignored_users) == 0 - await wait_for_debouncer(hass) - - sensor = hass.states.get("sensor.plex_plex_server_1") - assert sensor.state == str(len(mock_plex_server.accounts)) - async def test_new_ignored_users_available( - hass, caplog, entry, mock_websocket, setup_plex_server + hass, + caplog, + entry, + mock_websocket, + setup_plex_server, + requests_mock, + session_new_user, ): """Test setting up when new users available on Plex server but are ignored.""" - MONITORED_USERS = {"Owner": {"enabled": True}} + MONITORED_USERS = {"User 1": {"enabled": True}} OPTIONS_WITH_USERS = copy.deepcopy(DEFAULT_OPTIONS) OPTIONS_WITH_USERS[MP_DOMAIN][CONF_MONITORED_USERS] = MONITORED_USERS OPTIONS_WITH_USERS[MP_DOMAIN][CONF_IGNORE_NEW_SHARED_USERS] = True @@ -77,43 +66,50 @@ async def test_new_ignored_users_available( mock_plex_server = await setup_plex_server(config_entry=entry) - server_id = mock_plex_server.machineIdentifier + requests_mock.get( + f"{mock_plex_server.url_in_use}/status/sessions", + text=session_new_user, + ) + trigger_plex_update(mock_websocket) + await wait_for_debouncer(hass) + server_id = mock_plex_server.machine_identifier + + active_sessions = mock_plex_server._plex_server.sessions() monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users - ignored_users = [x for x in mock_plex_server.accounts if x not in monitored_users] + assert len(monitored_users) == 1 assert len(ignored_users) == 2 + for ignored_user in ignored_users: ignored_client = [ - x.players[0] - for x in mock_plex_server.sessions() - if x.usernames[0] == ignored_user - ][0] - assert ( - f"Ignoring {ignored_client.product} client owned by '{ignored_user}'" - in caplog.text - ) + x.players[0] for x in active_sessions if x.usernames[0] == ignored_user + ] + if ignored_client: + assert ( + f"Ignoring {ignored_client[0].product} client owned by '{ignored_user}'" + in caplog.text + ) await wait_for_debouncer(hass) sensor = hass.states.get("sensor.plex_plex_server_1") - assert sensor.state == str(len(mock_plex_server.accounts)) + assert sensor.state == str(len(active_sessions)) -async def test_network_error_during_refresh( - hass, caplog, mock_plex_server, mock_websocket -): +async def test_network_error_during_refresh(hass, caplog, mock_plex_server): """Test network failures during refreshes.""" - server_id = mock_plex_server.machineIdentifier + server_id = mock_plex_server.machine_identifier loaded_server = hass.data[DOMAIN][SERVERS][server_id] + active_sessions = mock_plex_server._plex_server.sessions() await wait_for_debouncer(hass) sensor = hass.states.get("sensor.plex_plex_server_1") - assert sensor.state == str(len(mock_plex_server.accounts)) + assert sensor.state == str(len(active_sessions)) - with patch.object(mock_plex_server, "clients", side_effect=RequestException): + with patch("plexapi.server.PlexServer.clients", side_effect=RequestException): await loaded_server._async_update_platforms() await hass.async_block_till_done() @@ -130,25 +126,31 @@ async def test_gdm_client_failure(hass, mock_websocket, setup_plex_server): mock_plex_server = await setup_plex_server(disable_gdm=False) await hass.async_block_till_done() + active_sessions = mock_plex_server._plex_server.sessions() await wait_for_debouncer(hass) sensor = hass.states.get("sensor.plex_plex_server_1") - assert sensor.state == str(len(mock_plex_server.accounts)) + assert sensor.state == str(len(active_sessions)) - with patch.object(mock_plex_server, "clients", side_effect=RequestException): + with patch("plexapi.server.PlexServer.clients", side_effect=RequestException): trigger_plex_update(mock_websocket) await hass.async_block_till_done() -async def test_mark_sessions_idle(hass, mock_plex_server, mock_websocket): +async def test_mark_sessions_idle( + hass, mock_plex_server, mock_websocket, requests_mock, empty_payload +): """Test marking media_players as idle when sessions end.""" await wait_for_debouncer(hass) - sensor = hass.states.get("sensor.plex_plex_server_1") - assert sensor.state == str(len(mock_plex_server.accounts)) + active_sessions = mock_plex_server._plex_server.sessions() - mock_plex_server.clear_clients() - mock_plex_server.clear_sessions() + sensor = hass.states.get("sensor.plex_plex_server_1") + assert sensor.state == str(len(active_sessions)) + + url = mock_plex_server.url_in_use + requests_mock.get(f"{url}/clients", text=empty_payload) + requests_mock.get(f"{url}/status/sessions", text=empty_payload) trigger_plex_update(mock_websocket) await hass.async_block_till_done() @@ -158,43 +160,46 @@ async def test_mark_sessions_idle(hass, mock_plex_server, mock_websocket): assert sensor.state == "0" -async def test_ignore_plex_web_client(hass, entry, mock_websocket, setup_plex_server): +async def test_ignore_plex_web_client(hass, entry, setup_plex_server): """Test option to ignore Plex Web clients.""" OPTIONS = copy.deepcopy(DEFAULT_OPTIONS) OPTIONS[MP_DOMAIN][CONF_IGNORE_PLEX_WEB_CLIENTS] = True entry.options = OPTIONS - with patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount(players=0)): - mock_plex_server = await setup_plex_server(config_entry=entry) - await wait_for_debouncer(hass) + mock_plex_server = await setup_plex_server( + config_entry=entry, client_type="plexweb", disable_clients=True + ) + await wait_for_debouncer(hass) + active_sessions = mock_plex_server._plex_server.sessions() sensor = hass.states.get("sensor.plex_plex_server_1") - assert sensor.state == str(len(mock_plex_server.accounts)) + assert sensor.state == str(len(active_sessions)) media_players = hass.states.async_entity_ids("media_player") assert len(media_players) == int(sensor.state) - 1 -async def test_media_lookups(hass, mock_plex_server, mock_websocket): +async def test_media_lookups(hass, mock_plex_server, requests_mock, playqueue_created): """Test media lookups to Plex server.""" - server_id = mock_plex_server.machineIdentifier + server_id = mock_plex_server.machine_identifier loaded_server = hass.data[DOMAIN][SERVERS][server_id] # Plex Key searches media_player_id = hass.states.async_entity_ids("media_player")[0] - with patch("homeassistant.components.plex.PlexServer.create_playqueue"): - assert await hass.services.async_call( - MP_DOMAIN, - SERVICE_PLAY_MEDIA, - { - ATTR_ENTITY_ID: media_player_id, - ATTR_MEDIA_CONTENT_TYPE: DOMAIN, - ATTR_MEDIA_CONTENT_ID: 123, - }, - True, - ) - with patch.object(MockPlexServer, "fetchItem", side_effect=NotFound): + requests_mock.post("/playqueues", text=playqueue_created) + requests_mock.get("/player/playback/playMedia", status_code=200) + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player_id, + ATTR_MEDIA_CONTENT_TYPE: DOMAIN, + ATTR_MEDIA_CONTENT_ID: 1, + }, + True, + ) + with patch("plexapi.server.PlexServer.fetchItem", side_effect=NotFound): assert await hass.services.async_call( MP_DOMAIN, SERVICE_PLAY_MEDIA, @@ -207,20 +212,18 @@ async def test_media_lookups(hass, mock_plex_server, mock_websocket): ) # TV show searches - with patch.object(MockPlexLibrary, "section", side_effect=NotFound): - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_EPISODE, library_name="Not a Library", show_name="TV Show" - ) - is None + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_EPISODE, library_name="Not a Library", show_name="TV Show" ) - with patch.object(MockPlexLibrarySection, "get", side_effect=NotFound): - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_EPISODE, library_name="TV Shows", show_name="Not a TV Show" - ) - is None + is None + ) + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_EPISODE, library_name="TV Shows", show_name="Not a TV Show" ) + is None + ) assert ( loaded_server.lookup_media( MEDIA_TYPE_EPISODE, library_name="TV Shows", episode_name="An Episode" @@ -234,36 +237,34 @@ async def test_media_lookups(hass, mock_plex_server, mock_websocket): MEDIA_TYPE_EPISODE, library_name="TV Shows", show_name="TV Show", - season_number=2, + season_number=1, ) assert loaded_server.lookup_media( MEDIA_TYPE_EPISODE, library_name="TV Shows", show_name="TV Show", - season_number=2, + season_number=1, episode_number=3, ) - with patch.object(MockPlexShow, "season", side_effect=NotFound): - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_EPISODE, - library_name="TV Shows", - show_name="TV Show", - season_number=2, - ) - is None + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_EPISODE, + library_name="TV Shows", + show_name="TV Show", + season_number=2, ) - with patch.object(MockPlexSeason, "episode", side_effect=NotFound): - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_EPISODE, - library_name="TV Shows", - show_name="TV Show", - season_number=2, - episode_number=1, - ) - is None + is None + ) + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_EPISODE, + library_name="TV Shows", + show_name="TV Show", + season_number=2, + episode_number=1, ) + is None + ) # Music searches assert ( @@ -287,47 +288,43 @@ async def test_media_lookups(hass, mock_plex_server, mock_websocket): artist_name="Artist", album_name="Album", ) - with patch.object(MockPlexLibrarySection, "get", side_effect=NotFound): - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_MUSIC, - library_name="Music", - artist_name="Not an Artist", - album_name="Album", - ) - is None + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_MUSIC, + library_name="Music", + artist_name="Not an Artist", + album_name="Album", ) - with patch.object(MockPlexArtist, "album", side_effect=NotFound): - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_MUSIC, - library_name="Music", - artist_name="Artist", - album_name="Not an Album", - ) - is None + is None + ) + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_MUSIC, + library_name="Music", + artist_name="Artist", + album_name="Not an Album", ) - with patch.object(MockPlexAlbum, "track", side_effect=NotFound): - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_MUSIC, - library_name="Music", - artist_name="Artist", - album_name=" Album", - track_name="Not a Track", - ) - is None + is None + ) + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_MUSIC, + library_name="Music", + artist_name="Artist", + album_name=" Album", + track_name="Not a Track", ) - with patch.object(MockPlexArtist, "get", side_effect=NotFound): - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_MUSIC, - library_name="Music", - artist_name="Artist", - track_name="Not a Track", - ) - is None + is None + ) + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_MUSIC, + library_name="Music", + artist_name="Artist", + track_name="Not a Track", ) + is None + ) assert loaded_server.lookup_media( MEDIA_TYPE_MUSIC, library_name="Music", @@ -354,44 +351,33 @@ async def test_media_lookups(hass, mock_plex_server, mock_websocket): ) # Playlist searches - assert loaded_server.lookup_media(MEDIA_TYPE_PLAYLIST, playlist_name="A Playlist") + assert loaded_server.lookup_media(MEDIA_TYPE_PLAYLIST, playlist_name="Playlist 1") assert loaded_server.lookup_media(MEDIA_TYPE_PLAYLIST) is None - with patch.object(MockPlexServer, "playlist", side_effect=NotFound): - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_PLAYLIST, playlist_name="Not a Playlist" - ) - is None - ) + assert ( + loaded_server.lookup_media(MEDIA_TYPE_PLAYLIST, playlist_name="Not a Playlist") + is None + ) # Legacy Movie searches assert loaded_server.lookup_media(MEDIA_TYPE_VIDEO, video_name="Movie") is None assert loaded_server.lookup_media(MEDIA_TYPE_VIDEO, library_name="Movies") is None assert loaded_server.lookup_media( - MEDIA_TYPE_VIDEO, library_name="Movies", video_name="Movie" + MEDIA_TYPE_VIDEO, library_name="Movies", video_name="Movie 1" ) - with patch.object(MockPlexLibrarySection, "get", side_effect=NotFound): - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_VIDEO, library_name="Movies", video_name="Not a Movie" - ) - is None + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_VIDEO, library_name="Movies", video_name="Not a Movie" ) + is None + ) # Movie searches assert loaded_server.lookup_media(MEDIA_TYPE_MOVIE, title="Movie") is None assert loaded_server.lookup_media(MEDIA_TYPE_MOVIE, library_name="Movies") is None assert loaded_server.lookup_media( - MEDIA_TYPE_MOVIE, library_name="Movies", title="Movie" + MEDIA_TYPE_MOVIE, library_name="Movies", title="Movie 1" ) - with patch.object(MockPlexLibrarySection, "search", side_effect=BadRequest): - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_MOVIE, library_name="Movies", title="Not a Movie" - ) - is None - ) - with patch.object(MockPlexLibrarySection, "search", return_value=[]): + with patch("plexapi.library.LibrarySection.search", side_effect=BadRequest): assert ( loaded_server.lookup_media( MEDIA_TYPE_MOVIE, library_name="Movies", title="Not a Movie" @@ -399,25 +385,8 @@ async def test_media_lookups(hass, mock_plex_server, mock_websocket): is None ) - similar_movies = [] - for title in "Duplicate Movie", "Duplicate Movie 2": - similar_movies.append(MockPlexMediaItem(title)) - with patch.object( - loaded_server.library.section("Movies"), "search", return_value=similar_movies - ): - found_media = loaded_server.lookup_media( - MEDIA_TYPE_MOVIE, library_name="Movies", title="Duplicate Movie" + assert ( + loaded_server.lookup_media( + MEDIA_TYPE_MOVIE, library_name="Movies", title="Movie" ) - assert found_media.title == "Duplicate Movie" - - duplicate_movies = [] - for title in "Duplicate Movie - Original", "Duplicate Movie - Remake": - duplicate_movies.append(MockPlexMediaItem(title)) - with patch.object( - loaded_server.library.section("Movies"), "search", return_value=duplicate_movies - ): - assert ( - loaded_server.lookup_media( - MEDIA_TYPE_MOVIE, library_name="Movies", title="Duplicate Movie" - ) - ) is None + ) is None diff --git a/tests/components/plex/test_services.py b/tests/components/plex/test_services.py index a3f4d4c833a..cf8bc63c5da 100644 --- a/tests/components/plex/test_services.py +++ b/tests/components/plex/test_services.py @@ -1,4 +1,10 @@ """Tests for various Plex services.""" +from unittest.mock import patch + +from plexapi.exceptions import NotFound +import pytest + +from homeassistant.components.media_player.const import MEDIA_TYPE_MUSIC from homeassistant.components.plex.const import ( CONF_SERVER, CONF_SERVER_IDENTIFIER, @@ -7,78 +13,89 @@ from homeassistant.components.plex.const import ( SERVICE_REFRESH_LIBRARY, SERVICE_SCAN_CLIENTS, ) -from homeassistant.const import ( - CONF_HOST, - CONF_PORT, - CONF_TOKEN, - CONF_URL, - CONF_VERIFY_SSL, -) +from homeassistant.components.plex.services import play_on_sonos +from homeassistant.const import CONF_URL +from homeassistant.exceptions import HomeAssistantError -from .const import MOCK_SERVERS, MOCK_TOKEN -from .mock_classes import MockPlexLibrarySection +from .const import DEFAULT_OPTIONS, SECONDARY_DATA -from tests.async_mock import patch from tests.common import MockConfigEntry -async def test_refresh_library(hass, mock_plex_server, setup_plex_server): +async def test_refresh_library( + hass, + mock_plex_server, + setup_plex_server, + requests_mock, + empty_payload, + plex_server_accounts, + plex_server_base, +): """Test refresh_library service call.""" + url = mock_plex_server.url_in_use + refresh = requests_mock.get(f"{url}/library/sections/1/refresh", status_code=200) + # Test with non-existent server - with patch.object(MockPlexLibrarySection, "update") as mock_update: + with pytest.raises(HomeAssistantError): assert await hass.services.async_call( DOMAIN, SERVICE_REFRESH_LIBRARY, {"server_name": "Not a Server", "library_name": "Movies"}, True, ) - assert not mock_update.called + assert not refresh.called # Test with non-existent library - with patch.object(MockPlexLibrarySection, "update") as mock_update: - assert await hass.services.async_call( - DOMAIN, - SERVICE_REFRESH_LIBRARY, - {"library_name": "Not a Library"}, - True, - ) - assert not mock_update.called + assert await hass.services.async_call( + DOMAIN, + SERVICE_REFRESH_LIBRARY, + {"library_name": "Not a Library"}, + True, + ) + assert not refresh.called # Test with valid library - with patch.object(MockPlexLibrarySection, "update") as mock_update: - assert await hass.services.async_call( - DOMAIN, - SERVICE_REFRESH_LIBRARY, - {"library_name": "Movies"}, - True, - ) - assert mock_update.called + assert await hass.services.async_call( + DOMAIN, + SERVICE_REFRESH_LIBRARY, + {"library_name": "Movies"}, + True, + ) + assert refresh.call_count == 1 # Add a second configured server + secondary_url = SECONDARY_DATA[PLEX_SERVER_CONFIG][CONF_URL] + secondary_name = SECONDARY_DATA[CONF_SERVER] + secondary_id = SECONDARY_DATA[CONF_SERVER_IDENTIFIER] + requests_mock.get( + secondary_url, + text=plex_server_base.format( + name=secondary_name, machine_identifier=secondary_id + ), + ) + requests_mock.get(f"{secondary_url}/accounts", text=plex_server_accounts) + requests_mock.get(f"{secondary_url}/clients", text=empty_payload) + requests_mock.get(f"{secondary_url}/status/sessions", text=empty_payload) + entry_2 = MockConfigEntry( domain=DOMAIN, - data={ - CONF_SERVER: MOCK_SERVERS[1][CONF_SERVER], - PLEX_SERVER_CONFIG: { - CONF_TOKEN: MOCK_TOKEN, - CONF_URL: f"https://{MOCK_SERVERS[1][CONF_HOST]}:{MOCK_SERVERS[1][CONF_PORT]}", - CONF_VERIFY_SSL: True, - }, - CONF_SERVER_IDENTIFIER: MOCK_SERVERS[1][CONF_SERVER_IDENTIFIER], - }, + data=SECONDARY_DATA, + options=DEFAULT_OPTIONS, + unique_id=SECONDARY_DATA["server_id"], ) await setup_plex_server(config_entry=entry_2) # Test multiple servers available but none specified - with patch.object(MockPlexLibrarySection, "update") as mock_update: + with pytest.raises(HomeAssistantError) as excinfo: assert await hass.services.async_call( DOMAIN, SERVICE_REFRESH_LIBRARY, {"library_name": "Movies"}, True, ) - assert not mock_update.called + assert "Multiple Plex servers configured" in str(excinfo.value) + assert refresh.call_count == 1 async def test_scan_clients(hass, mock_plex_server): @@ -88,3 +105,93 @@ async def test_scan_clients(hass, mock_plex_server): SERVICE_SCAN_CLIENTS, blocking=True, ) + + +async def test_sonos_play_media( + hass, + entry, + setup_plex_server, + requests_mock, + empty_payload, + playqueue_1234, + playqueue_created, + plextv_account, + sonos_resources, +): + """Test playback from a Sonos media_player.play_media call.""" + media_content_id = ( + '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album"}' + ) + sonos_speaker_name = "Zone A" + + requests_mock.get("https://plex.tv/users/account", text=plextv_account) + requests_mock.post("/playqueues", text=playqueue_created) + playback_mock = requests_mock.get("/player/playback/playMedia", status_code=200) + + # Test with no Plex integration available + with pytest.raises(HomeAssistantError) as excinfo: + play_on_sonos(hass, MEDIA_TYPE_MUSIC, media_content_id, sonos_speaker_name) + assert "Plex integration not configured" in str(excinfo.value) + + with patch( + "homeassistant.components.plex.PlexServer.connect", side_effect=NotFound + ): + # Initialize Plex integration without setting up a server + with pytest.raises(AssertionError): + await setup_plex_server() + + # Test with no Plex servers available + with pytest.raises(HomeAssistantError) as excinfo: + play_on_sonos(hass, MEDIA_TYPE_MUSIC, media_content_id, sonos_speaker_name) + assert "No Plex servers available" in str(excinfo.value) + + # Complete setup of a Plex server + await hass.config_entries.async_unload(entry.entry_id) + mock_plex_server = await setup_plex_server() + + # Test with no speakers available + requests_mock.get("https://sonos.plex.tv/resources", text=empty_payload) + with pytest.raises(HomeAssistantError) as excinfo: + play_on_sonos(hass, MEDIA_TYPE_MUSIC, media_content_id, sonos_speaker_name) + assert f"Sonos speaker '{sonos_speaker_name}' is not associated with" in str( + excinfo.value + ) + assert playback_mock.call_count == 0 + + # Test with speakers available + requests_mock.get("https://sonos.plex.tv/resources", text=sonos_resources) + with patch.object(mock_plex_server.account, "_sonos_cache_timestamp", 0): + play_on_sonos(hass, MEDIA_TYPE_MUSIC, media_content_id, sonos_speaker_name) + assert playback_mock.call_count == 1 + + # Test with speakers available and media key payload + play_on_sonos(hass, MEDIA_TYPE_MUSIC, "100", sonos_speaker_name) + assert playback_mock.call_count == 2 + + # Test with speakers available and Plex server specified + content_id_with_server = '{"plex_server": "Plex Server 1", "library_name": "Music", "artist_name": "Artist", "album_name": "Album"}' + play_on_sonos(hass, MEDIA_TYPE_MUSIC, content_id_with_server, sonos_speaker_name) + assert playback_mock.call_count == 3 + + # Test with speakers available but media not found + content_id_bad_media = '{"library_name": "Music", "artist_name": "Not an Artist"}' + with pytest.raises(HomeAssistantError) as excinfo: + play_on_sonos(hass, MEDIA_TYPE_MUSIC, content_id_bad_media, sonos_speaker_name) + assert "Plex media not found" in str(excinfo.value) + assert playback_mock.call_count == 3 + + # Test with speakers available and playqueue + requests_mock.get("https://1.2.3.4:32400/playQueues/1234", text=playqueue_1234) + content_id_with_playqueue = '{"playqueue_id": 1234}' + play_on_sonos(hass, MEDIA_TYPE_MUSIC, content_id_with_playqueue, sonos_speaker_name) + assert playback_mock.call_count == 4 + + # Test with speakers available and invalid playqueue + requests_mock.get("https://1.2.3.4:32400/playQueues/1235", status_code=404) + content_id_with_playqueue = '{"playqueue_id": 1235}' + with pytest.raises(HomeAssistantError) as excinfo: + play_on_sonos( + hass, MEDIA_TYPE_MUSIC, content_id_with_playqueue, sonos_speaker_name + ) + assert "PlayQueue '1235' could not be found" in str(excinfo.value) + assert playback_mock.call_count == 4 diff --git a/tests/components/plugwise/conftest.py b/tests/components/plugwise/conftest.py index ae934c565bc..615cfc55eeb 100644 --- a/tests/components/plugwise/conftest.py +++ b/tests/components/plugwise/conftest.py @@ -2,6 +2,7 @@ from functools import partial import re +from unittest.mock import AsyncMock, Mock, patch import jsonpickle from plugwise.exceptions import ( @@ -12,7 +13,6 @@ from plugwise.exceptions import ( ) import pytest -from tests.async_mock import AsyncMock, Mock, patch from tests.common import load_fixture from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index fc0e5f9e69f..382e7bc1a52 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Plugwise config flow.""" +from unittest.mock import MagicMock, patch + from plugwise.exceptions import ( ConnectionFailedError, InvalidAuthentication, @@ -22,7 +24,6 @@ from homeassistant.const import ( CONF_USERNAME, ) -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry TEST_HOST = "1.1.1.1" diff --git a/tests/components/plugwise/test_sensor.py b/tests/components/plugwise/test_sensor.py index a722749496f..a2bf4ebc50e 100644 --- a/tests/components/plugwise/test_sensor.py +++ b/tests/components/plugwise/test_sensor.py @@ -65,13 +65,13 @@ async def test_p1_dsmr_sensor_entities(hass, mock_smile_p1): assert float(state.state) == -2761.0 state = hass.states.get("sensor.p1_electricity_consumed_off_peak_cumulative") - assert float(state.state) == 551.1 + assert float(state.state) == 551.09 state = hass.states.get("sensor.p1_electricity_produced_peak_point") assert float(state.state) == 2761.0 state = hass.states.get("sensor.p1_electricity_consumed_peak_cumulative") - assert float(state.state) == 442.9 + assert float(state.state) == 442.932 state = hass.states.get("sensor.p1_gas_consumed_cumulative") assert float(state.state) == 584.85 @@ -83,7 +83,7 @@ async def test_stretch_sensor_entities(hass, mock_stretch): assert entry.state == ENTRY_STATE_LOADED state = hass.states.get("sensor.koelkast_92c4a_electricity_consumed") - assert float(state.state) == 53.2 + assert float(state.state) == 50.5 state = hass.states.get("sensor.droger_52559_electricity_consumed_interval") - assert float(state.state) == 1.06 + assert float(state.state) == 0.0 diff --git a/tests/components/plum_lightpad/test_config_flow.py b/tests/components/plum_lightpad/test_config_flow.py index f2ac756c4a3..29539b5886f 100644 --- a/tests/components/plum_lightpad/test_config_flow.py +++ b/tests/components/plum_lightpad/test_config_flow.py @@ -1,10 +1,11 @@ """Test the Plum Lightpad config flow.""" +from unittest.mock import patch + from requests.exceptions import ConnectTimeout from homeassistant import config_entries, setup from homeassistant.components.plum_lightpad.const import DOMAIN -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/plum_lightpad/test_init.py b/tests/components/plum_lightpad/test_init.py index 139ab786d1d..f4452dce880 100644 --- a/tests/components/plum_lightpad/test_init.py +++ b/tests/components/plum_lightpad/test_init.py @@ -1,4 +1,6 @@ """Tests for the Plum Lightpad config flow.""" +from unittest.mock import Mock, patch + from aiohttp import ContentTypeError from requests.exceptions import HTTPError @@ -6,7 +8,6 @@ from homeassistant.components.plum_lightpad.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/point/test_config_flow.py b/tests/components/point/test_config_flow.py index 67817b308ce..93ea18f21b6 100644 --- a/tests/components/point/test_config_flow.py +++ b/tests/components/point/test_config_flow.py @@ -1,5 +1,6 @@ """Tests for the Point config flow.""" import asyncio +from unittest.mock import AsyncMock, patch import pytest @@ -7,8 +8,6 @@ from homeassistant import data_entry_flow from homeassistant.components.point import DOMAIN, config_flow from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET -from tests.async_mock import AsyncMock, patch - def init_config_flow(hass, side_effect=None): """Init a configuration flow.""" diff --git a/tests/components/poolsense/test_config_flow.py b/tests/components/poolsense/test_config_flow.py index 8ea05339c84..ca32a21758e 100644 --- a/tests/components/poolsense/test_config_flow.py +++ b/tests/components/poolsense/test_config_flow.py @@ -1,11 +1,11 @@ """Test the PoolSense config flow.""" +from unittest.mock import patch + from homeassistant import data_entry_flow from homeassistant.components.poolsense.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from tests.async_mock import patch - async def test_show_form(hass): """Test that the form is served with no input.""" diff --git a/tests/components/powerwall/mocks.py b/tests/components/powerwall/mocks.py index 6beb31c293a..0e3cec7f60b 100644 --- a/tests/components/powerwall/mocks.py +++ b/tests/components/powerwall/mocks.py @@ -2,6 +2,7 @@ import json import os +from unittest.mock import MagicMock, Mock from tesla_powerwall import ( DeviceType, @@ -13,10 +14,6 @@ from tesla_powerwall import ( SiteMaster, ) -from homeassistant.components.powerwall.const import DOMAIN -from homeassistant.const import CONF_IP_ADDRESS - -from tests.async_mock import MagicMock, Mock from tests.common import load_fixture @@ -85,8 +82,3 @@ async def _async_load_json_fixture(hass, path): load_fixture, os.path.join("powerwall", path) ) return json.loads(fixture) - - -def _mock_get_config(): - """Return a default powerwall config.""" - return {DOMAIN: {CONF_IP_ADDRESS: "1.2.3.4"}} diff --git a/tests/components/powerwall/test_binary_sensor.py b/tests/components/powerwall/test_binary_sensor.py index caf7519f598..006ecdfb533 100644 --- a/tests/components/powerwall/test_binary_sensor.py +++ b/tests/components/powerwall/test_binary_sensor.py @@ -1,12 +1,13 @@ """The binary sensor tests for the powerwall platform.""" +from unittest.mock import patch + from homeassistant.components.powerwall.const import DOMAIN -from homeassistant.const import STATE_ON -from homeassistant.setup import async_setup_component +from homeassistant.const import CONF_IP_ADDRESS, STATE_ON -from .mocks import _mock_get_config, _mock_powerwall_with_fixtures +from .mocks import _mock_powerwall_with_fixtures -from tests.async_mock import patch +from tests.common import MockConfigEntry async def test_sensors(hass): @@ -14,14 +15,15 @@ async def test_sensors(hass): mock_powerwall = await _mock_powerwall_with_fixtures(hass) + config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_IP_ADDRESS: "1.2.3.4"}) + config_entry.add_to_hass(hass) with patch( "homeassistant.components.powerwall.config_flow.Powerwall", return_value=mock_powerwall, ), patch( - "homeassistant.components.powerwall.Powerwall", - return_value=mock_powerwall, + "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall ): - assert await async_setup_component(hass, DOMAIN, _mock_get_config()) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() state = hass.states.get("binary_sensor.grid_status") diff --git a/tests/components/powerwall/test_config_flow.py b/tests/components/powerwall/test_config_flow.py index 4f1a587b31b..0955c16c9ec 100644 --- a/tests/components/powerwall/test_config_flow.py +++ b/tests/components/powerwall/test_config_flow.py @@ -1,14 +1,17 @@ """Test the Powerwall config flow.""" +from unittest.mock import patch + from tesla_powerwall import MissingAttributeError, PowerwallUnreachableError from homeassistant import config_entries, setup +from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.components.powerwall.const import DOMAIN from homeassistant.const import CONF_IP_ADDRESS from .mocks import _mock_powerwall_side_effect, _mock_powerwall_site_name -from tests.async_mock import patch +from tests.common import MockConfigEntry async def test_form_source_user(hass): @@ -44,34 +47,6 @@ async def test_form_source_user(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_source_import(hass): - """Test we setup the config entry via import.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - mock_powerwall = await _mock_powerwall_site_name(hass, "Imported site") - with patch( - "homeassistant.components.powerwall.config_flow.Powerwall", - return_value=mock_powerwall, - ), patch( - "homeassistant.components.powerwall.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.powerwall.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_IP_ADDRESS: "1.2.3.4"}, - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == "Imported site" - assert result["data"] == {CONF_IP_ADDRESS: "1.2.3.4"} - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - async def test_form_cannot_connect(hass): """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( @@ -93,6 +68,27 @@ async def test_form_cannot_connect(hass): assert result2["errors"] == {"base": "cannot_connect"} +async def test_form_unknown_exeption(hass): + """Test we handle an unknown exception.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_powerwall = _mock_powerwall_side_effect(site_info=ValueError) + + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_IP_ADDRESS: "1.2.3.4"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} + + async def test_form_wrong_version(hass): """Test we can handle wrong version error.""" result = await hass.config_entries.flow.async_init( @@ -114,3 +110,84 @@ async def test_form_wrong_version(hass): assert result3["type"] == "form" assert result3["errors"] == {"base": "wrong_version"} + + +async def test_already_configured(hass): + """Test we abort when already configured.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_IP_ADDRESS: "1.1.1.1"}) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={ + IP_ADDRESS: "1.1.1.1", + MAC_ADDRESS: "AA:BB:CC:DD:EE:FF", + HOSTNAME: "any", + }, + ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_already_configured_with_ignored(hass): + """Test ignored entries do not break checking for existing entries.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + config_entry = MockConfigEntry(domain=DOMAIN, data={}, source="ignore") + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={ + IP_ADDRESS: "1.1.1.1", + MAC_ADDRESS: "AA:BB:CC:DD:EE:FF", + HOSTNAME: "any", + }, + ) + assert result["type"] == "form" + + +async def test_dhcp_discovery(hass): + """Test we can process the discovery from dhcp.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={ + IP_ADDRESS: "1.1.1.1", + MAC_ADDRESS: "AA:BB:CC:DD:EE:FF", + HOSTNAME: "any", + }, + ) + assert result["type"] == "form" + assert result["errors"] == {} + + mock_powerwall = await _mock_powerwall_site_name(hass, "Some site") + with patch( + "homeassistant.components.powerwall.config_flow.Powerwall", + return_value=mock_powerwall, + ), patch( + "homeassistant.components.powerwall.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.powerwall.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_IP_ADDRESS: "1.1.1.1", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Some site" + assert result2["data"] == { + CONF_IP_ADDRESS: "1.1.1.1", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/powerwall/test_sensor.py b/tests/components/powerwall/test_sensor.py index 0be7c12c5a8..eff4631c7e5 100644 --- a/tests/components/powerwall/test_sensor.py +++ b/tests/components/powerwall/test_sensor.py @@ -1,12 +1,13 @@ """The sensor tests for the powerwall platform.""" +from unittest.mock import patch + from homeassistant.components.powerwall.const import DOMAIN -from homeassistant.const import PERCENTAGE -from homeassistant.setup import async_setup_component +from homeassistant.const import CONF_IP_ADDRESS, PERCENTAGE -from .mocks import _mock_get_config, _mock_powerwall_with_fixtures +from .mocks import _mock_powerwall_with_fixtures -from tests.async_mock import patch +from tests.common import MockConfigEntry async def test_sensors(hass): @@ -14,19 +15,20 @@ async def test_sensors(hass): mock_powerwall = await _mock_powerwall_with_fixtures(hass) + config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_IP_ADDRESS: "1.2.3.4"}) + config_entry.add_to_hass(hass) with patch( "homeassistant.components.powerwall.config_flow.Powerwall", return_value=mock_powerwall, ), patch( "homeassistant.components.powerwall.Powerwall", return_value=mock_powerwall ): - assert await async_setup_component(hass, DOMAIN, _mock_get_config()) + assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() device_registry = await hass.helpers.device_registry.async_get_registry() reg_device = device_registry.async_get_device( identifiers={("powerwall", "TG0123456789AB_TG9876543210BA")}, - connections=set(), ) assert reg_device.model == "PowerWall 2 (GW1)" assert reg_device.sw_version == "1.45.1" diff --git a/tests/components/profiler/test_config_flow.py b/tests/components/profiler/test_config_flow.py index dc2ddff14d9..d3b2b473012 100644 --- a/tests/components/profiler/test_config_flow.py +++ b/tests/components/profiler/test_config_flow.py @@ -1,8 +1,9 @@ """Test the Profiler config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, setup from homeassistant.components.profiler.const import DOMAIN -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/profiler/test_init.py b/tests/components/profiler/test_init.py index 0ccda904eb6..efed6ef6126 100644 --- a/tests/components/profiler/test_init.py +++ b/tests/components/profiler/test_init.py @@ -1,6 +1,7 @@ """Test the Profiler config flow.""" from datetime import timedelta import os +from unittest.mock import patch from homeassistant import setup from homeassistant.components.profiler import ( @@ -16,7 +17,6 @@ from homeassistant.components.profiler import ( from homeassistant.components.profiler.const import DOMAIN import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/progettihwsw/test_config_flow.py b/tests/components/progettihwsw/test_config_flow.py index 7a0dbd692c0..883c1acd33b 100644 --- a/tests/components/progettihwsw/test_config_flow.py +++ b/tests/components/progettihwsw/test_config_flow.py @@ -1,4 +1,6 @@ """Test the ProgettiHWSW Automation config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, setup from homeassistant.components.progettihwsw.const import DOMAIN from homeassistant.const import CONF_HOST, CONF_PORT @@ -8,7 +10,6 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) -from tests.async_mock import patch from tests.common import MockConfigEntry mock_value_step_user = { diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index f187429b151..bea42cc0888 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -1,6 +1,7 @@ """The tests for the Prometheus exporter.""" from dataclasses import dataclass import datetime +import unittest.mock as mock import pytest @@ -19,8 +20,6 @@ from homeassistant.core import split_entity_id from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -import tests.async_mock as mock - PROMETHEUS_PATH = "homeassistant.components.prometheus" diff --git a/tests/components/ps4/conftest.py b/tests/components/ps4/conftest.py index 42002404db2..155f1c6d5dd 100644 --- a/tests/components/ps4/conftest.py +++ b/tests/components/ps4/conftest.py @@ -1,7 +1,7 @@ """Test configuration for PS4.""" -import pytest +from unittest.mock import patch -from tests.async_mock import patch +import pytest @pytest.fixture @@ -18,6 +18,13 @@ def patch_save_json(): yield mock_save +@pytest.fixture +def patch_get_status(): + """Prevent save JSON being used.""" + with patch("pyps4_2ndscreen.ps4.get_status", return_value=None) as mock_get_status: + yield mock_get_status + + @pytest.fixture(autouse=True) -def patch_io(patch_load_json, patch_save_json): +def patch_io(patch_load_json, patch_save_json, patch_get_status): """Prevent PS4 doing I/O.""" diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py index 8ef11335199..bcae74c19fb 100644 --- a/tests/components/ps4/test_config_flow.py +++ b/tests/components/ps4/test_config_flow.py @@ -1,4 +1,6 @@ """Define tests for the PlayStation 4 config flow.""" +from unittest.mock import patch + from pyps4_2ndscreen.errors import CredentialTimeout import pytest @@ -21,7 +23,6 @@ from homeassistant.const import ( ) from homeassistant.util import location -from tests.async_mock import patch from tests.common import MockConfigEntry MOCK_TITLE = "PlayStation 4" diff --git a/tests/components/ps4/test_init.py b/tests/components/ps4/test_init.py index 75a983cc97a..da241022938 100644 --- a/tests/components/ps4/test_init.py +++ b/tests/components/ps4/test_init.py @@ -1,4 +1,6 @@ """Tests for the PS4 Integration.""" +from unittest.mock import MagicMock, patch + from homeassistant import config_entries, data_entry_flow from homeassistant.components import ps4 from homeassistant.components.media_player.const import ( @@ -27,7 +29,6 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component from homeassistant.util import location -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry, mock_registry MOCK_HOST = "192.168.0.1" diff --git a/tests/components/ps4/test_media_player.py b/tests/components/ps4/test_media_player.py index 8a03f13beda..d402cbb01ae 100644 --- a/tests/components/ps4/test_media_player.py +++ b/tests/components/ps4/test_media_player.py @@ -1,4 +1,6 @@ """Tests for the PS4 media player platform.""" +from unittest.mock import MagicMock, patch + from pyps4_2ndscreen.credential import get_ddp_message from pyps4_2ndscreen.ddp import DEFAULT_UDP_PORT from pyps4_2ndscreen.media_art import TYPE_APP as PS_TYPE_APP @@ -36,7 +38,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry, mock_device_registry, mock_registry MOCK_CREDS = "123412341234abcd12341234abcd12341234abcd12341234abcd12341234abcd" @@ -287,11 +288,11 @@ async def test_media_attributes_are_loaded(hass, patch_load_json): assert mock_attrs.get(ATTR_MEDIA_CONTENT_TYPE) == MOCK_TITLE_TYPE -async def test_device_info_is_set_from_status_correctly(hass): +async def test_device_info_is_set_from_status_correctly(hass, patch_get_status): """Test that device info is set correctly from status update.""" mock_d_registry = mock_device_registry(hass) - with patch("pyps4_2ndscreen.ps4.get_status", return_value=MOCK_STATUS_STANDBY): - mock_entity_id = await setup_mock_component(hass) + patch_get_status.return_value = MOCK_STATUS_STANDBY + mock_entity_id = await setup_mock_component(hass) await hass.async_block_till_done() @@ -303,9 +304,7 @@ async def test_device_info_is_set_from_status_correctly(hass): mock_state = hass.states.get(mock_entity_id).state mock_d_entries = mock_d_registry.devices - mock_entry = mock_d_registry.async_get_device( - identifiers={(DOMAIN, MOCK_HOST_ID)}, connections={()} - ) + mock_entry = mock_d_registry.async_get_device(identifiers={(DOMAIN, MOCK_HOST_ID)}) assert mock_state == STATE_STANDBY assert len(mock_d_entries) == 1 diff --git a/tests/components/ptvsd/__init__py b/tests/components/ptvsd/__init__py deleted file mode 100644 index e2a1a9ba0a6..00000000000 --- a/tests/components/ptvsd/__init__py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for PTVSD Debugger""" diff --git a/tests/components/ptvsd/test_ptvsd.py b/tests/components/ptvsd/test_ptvsd.py deleted file mode 100644 index 93e1bb540db..00000000000 --- a/tests/components/ptvsd/test_ptvsd.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Tests for PTVSD Debugger.""" - -from pytest import mark - -from homeassistant.bootstrap import _async_set_up_integrations -import homeassistant.components.ptvsd as ptvsd_component -from homeassistant.setup import async_setup_component - -from tests.async_mock import AsyncMock, patch - - -@mark.skip("causes code cover to fail") -async def test_ptvsd(hass): - """Test loading ptvsd component.""" - with patch("ptvsd.enable_attach") as attach: - with patch("ptvsd.wait_for_attach") as wait: - assert await async_setup_component( - hass, ptvsd_component.DOMAIN, {ptvsd_component.DOMAIN: {}} - ) - - attach.assert_called_once_with(("0.0.0.0", 5678)) - assert wait.call_count == 0 - - -@mark.skip("causes code cover to fail") -async def test_ptvsd_wait(hass): - """Test loading ptvsd component with wait.""" - with patch("ptvsd.enable_attach") as attach: - with patch("ptvsd.wait_for_attach") as wait: - assert await async_setup_component( - hass, - ptvsd_component.DOMAIN, - {ptvsd_component.DOMAIN: {ptvsd_component.CONF_WAIT: True}}, - ) - - attach.assert_called_once_with(("0.0.0.0", 5678)) - assert wait.call_count == 1 - - -async def test_ptvsd_bootstrap(hass): - """Test loading ptvsd component with wait.""" - config = {ptvsd_component.DOMAIN: {ptvsd_component.CONF_WAIT: True}} - - with patch("homeassistant.components.ptvsd.async_setup", AsyncMock()) as setup_mock: - setup_mock.return_value = True - await _async_set_up_integrations(hass, config) - - assert setup_mock.call_count == 1 diff --git a/tests/components/pushbullet/test_notify.py b/tests/components/pushbullet/test_notify.py index 12bfc686d17..3eec106019c 100644 --- a/tests/components/pushbullet/test_notify.py +++ b/tests/components/pushbullet/test_notify.py @@ -1,5 +1,6 @@ """The tests for the pushbullet notification platform.""" import json +from unittest.mock import patch from pushbullet import PushBullet import pytest @@ -7,7 +8,6 @@ import pytest import homeassistant.components.notify as notify from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import assert_setup_component, load_fixture diff --git a/tests/components/pvpc_hourly_pricing/test_config_flow.py b/tests/components/pvpc_hourly_pricing/test_config_flow.py index 5f72875c26a..ad321181ec4 100644 --- a/tests/components/pvpc_hourly_pricing/test_config_flow.py +++ b/tests/components/pvpc_hourly_pricing/test_config_flow.py @@ -1,5 +1,6 @@ """Tests for the pvpc_hourly_pricing config_flow.""" from datetime import datetime +from unittest.mock import patch from pytz import timezone @@ -10,7 +11,6 @@ from homeassistant.helpers import entity_registry from .conftest import check_valid_state -from tests.async_mock import patch from tests.common import date_util from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/pvpc_hourly_pricing/test_sensor.py b/tests/components/pvpc_hourly_pricing/test_sensor.py index ca3dec1e891..2045ba52671 100644 --- a/tests/components/pvpc_hourly_pricing/test_sensor.py +++ b/tests/components/pvpc_hourly_pricing/test_sensor.py @@ -1,6 +1,7 @@ """Tests for the pvpc_hourly_pricing sensor component.""" from datetime import datetime, timedelta import logging +from unittest.mock import patch from pytz import timezone @@ -11,7 +12,6 @@ from homeassistant.setup import async_setup_component from .conftest import check_valid_state -from tests.async_mock import patch from tests.common import date_util from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/python_script/test_init.py b/tests/components/python_script/test_init.py index bcd846889fa..b58bd6eb469 100644 --- a/tests/components/python_script/test_init.py +++ b/tests/components/python_script/test_init.py @@ -1,11 +1,11 @@ """Test the python_script component.""" import logging +from unittest.mock import mock_open, patch from homeassistant.components.python_script import DOMAIN, FOLDER, execute from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.setup import async_setup_component -from tests.async_mock import mock_open, patch from tests.common import patch_yaml_files diff --git a/tests/components/qld_bushfire/test_geo_location.py b/tests/components/qld_bushfire/test_geo_location.py index 743d967c953..58974c55b45 100644 --- a/tests/components/qld_bushfire/test_geo_location.py +++ b/tests/components/qld_bushfire/test_geo_location.py @@ -1,5 +1,6 @@ """The tests for the Queensland Bushfire Alert Feed platform.""" import datetime +from unittest.mock import MagicMock, call, patch from homeassistant.components import geo_location from homeassistant.components.geo_location import ATTR_SOURCE @@ -27,7 +28,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import MagicMock, call, patch from tests.common import assert_setup_component, async_fire_time_changed CONFIG = {geo_location.DOMAIN: [{"platform": "qld_bushfire", CONF_RADIUS: 200}]} diff --git a/tests/components/qwikswitch/test_init.py b/tests/components/qwikswitch/test_init.py index 7d92d40f987..74e600b50e4 100644 --- a/tests/components/qwikswitch/test_init.py +++ b/tests/components/qwikswitch/test_init.py @@ -1,5 +1,6 @@ """Test qwikswitch sensors.""" import asyncio +from unittest.mock import Mock from aiohttp.client_exceptions import ClientError import pytest @@ -8,7 +9,6 @@ from yarl import URL from homeassistant.components.qwikswitch import DOMAIN as QWIKSWITCH from homeassistant.setup import async_setup_component -from tests.async_mock import Mock from tests.test_util.aiohttp import AiohttpClientMockResponse, MockLongPollSideEffect diff --git a/tests/components/rachio/test_config_flow.py b/tests/components/rachio/test_config_flow.py index 75d671262a1..6b0fc2e69cb 100644 --- a/tests/components/rachio/test_config_flow.py +++ b/tests/components/rachio/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Rachio config flow.""" +from unittest.mock import MagicMock, patch + from homeassistant import config_entries, setup from homeassistant.components.rachio.const import ( CONF_CUSTOM_URL, @@ -7,7 +9,6 @@ from homeassistant.components.rachio.const import ( ) from homeassistant.const import CONF_API_KEY -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry diff --git a/tests/components/radarr/test_sensor.py b/tests/components/radarr/test_sensor.py index aa6a2a02679..514b8f1b817 100644 --- a/tests/components/radarr/test_sensor.py +++ b/tests/components/radarr/test_sensor.py @@ -1,11 +1,11 @@ """The tests for the Radarr platform.""" +from unittest.mock import patch + import pytest from homeassistant.const import DATA_GIGABYTES from homeassistant.setup import async_setup_component -from tests.async_mock import patch - def mocked_exception(*args, **kwargs): """Mock exception thrown by requests.get.""" diff --git a/tests/components/rainmachine/test_config_flow.py b/tests/components/rainmachine/test_config_flow.py index 7fb30d0043d..e79874831fe 100644 --- a/tests/components/rainmachine/test_config_flow.py +++ b/tests/components/rainmachine/test_config_flow.py @@ -1,4 +1,6 @@ """Define tests for the OpenUV config flow.""" +from unittest.mock import patch + from regenmaschine.errors import RainMachineError from homeassistant import data_entry_flow @@ -6,7 +8,6 @@ from homeassistant.components.rainmachine import CONF_ZONE_RUN_TIME, DOMAIN, con from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/random/test_binary_sensor.py b/tests/components/random/test_binary_sensor.py index 85a8e32018c..682c6db58d8 100644 --- a/tests/components/random/test_binary_sensor.py +++ b/tests/components/random/test_binary_sensor.py @@ -1,7 +1,7 @@ """The test for the Random binary sensor platform.""" -from homeassistant.setup import async_setup_component +from unittest.mock import patch -from tests.async_mock import patch +from homeassistant.setup import async_setup_component async def test_random_binary_sensor_on(hass): diff --git a/tests/components/recollect_waste/test_config_flow.py b/tests/components/recollect_waste/test_config_flow.py index ca3fbe8be56..cabcb1a8f9e 100644 --- a/tests/components/recollect_waste/test_config_flow.py +++ b/tests/components/recollect_waste/test_config_flow.py @@ -1,4 +1,6 @@ """Define tests for the ReCollect Waste config flow.""" +from unittest.mock import patch + from aiorecollect.errors import RecollectError from homeassistant import data_entry_flow @@ -10,7 +12,6 @@ from homeassistant.components.recollect_waste import ( from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_FRIENDLY_NAME -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index 41c1f52b993..d4092d709c0 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -1,6 +1,7 @@ """The tests for the Recorder component.""" # pylint: disable=protected-access from datetime import datetime, timedelta +from unittest.mock import patch from sqlalchemy.exc import OperationalError @@ -22,7 +23,6 @@ from homeassistant.util import dt as dt_util from .common import wait_recording_done -from tests.async_mock import patch from tests.common import fire_time_changed, get_test_home_assistant diff --git a/tests/components/recorder/test_migrate.py b/tests/components/recorder/test_migrate.py index d3cf69fc994..d10dad43d75 100644 --- a/tests/components/recorder/test_migrate.py +++ b/tests/components/recorder/test_migrate.py @@ -1,4 +1,7 @@ """The tests for the Recorder component.""" +# pylint: disable=protected-access +from unittest.mock import call, patch + import pytest from sqlalchemy import create_engine from sqlalchemy.pool import StaticPool @@ -6,8 +9,6 @@ from sqlalchemy.pool import StaticPool from homeassistant.bootstrap import async_setup_component from homeassistant.components.recorder import const, migration, models -# pylint: disable=protected-access -from tests.async_mock import call, patch from tests.components.recorder import models_original diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 9cb07819e79..791bd84b11b 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -1,6 +1,7 @@ """Test data purging.""" from datetime import datetime, timedelta import json +from unittest.mock import patch from homeassistant.components import recorder from homeassistant.components.recorder.const import DATA_INSTANCE @@ -11,8 +12,6 @@ from homeassistant.util import dt as dt_util from .common import wait_recording_done -from tests.async_mock import patch - def test_purge_old_states(hass, hass_recorder): """Test deleting old states.""" diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index 23ab7ff929d..a4109648d2f 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -2,6 +2,7 @@ from datetime import timedelta import os import sqlite3 +from unittest.mock import MagicMock, patch import pytest @@ -11,7 +12,6 @@ from homeassistant.util import dt as dt_util from .common import wait_recording_done -from tests.async_mock import MagicMock, patch from tests.common import get_test_home_assistant, init_recorder_component diff --git a/tests/components/reddit/test_sensor.py b/tests/components/reddit/test_sensor.py index c49abd4038a..21767792afd 100644 --- a/tests/components/reddit/test_sensor.py +++ b/tests/components/reddit/test_sensor.py @@ -1,5 +1,6 @@ """The tests for the Reddit platform.""" import copy +from unittest.mock import patch from homeassistant.components.reddit.sensor import ( ATTR_BODY, @@ -23,8 +24,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import patch - VALID_CONFIG = { "sensor": { "platform": DOMAIN, diff --git a/tests/components/remember_the_milk/test_init.py b/tests/components/remember_the_milk/test_init.py index e03475c9131..2f3511aeaa8 100644 --- a/tests/components/remember_the_milk/test_init.py +++ b/tests/components/remember_the_milk/test_init.py @@ -1,10 +1,10 @@ """Tests for the Remember The Milk component.""" +from unittest.mock import Mock, mock_open, patch + import homeassistant.components.remember_the_milk as rtm from .const import JSON_STRING, PROFILE, TOKEN -from tests.async_mock import Mock, mock_open, patch - def test_create_new(hass): """Test creating a new config file.""" diff --git a/tests/components/remote/test_device_condition.py b/tests/components/remote/test_device_condition.py index e5a9bc3a9c9..c6a2b3f0c52 100644 --- a/tests/components/remote/test_device_condition.py +++ b/tests/components/remote/test_device_condition.py @@ -1,5 +1,6 @@ """The test for remote device automation.""" from datetime import timedelta +from unittest.mock import patch import pytest @@ -10,7 +11,6 @@ from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import ( MockConfigEntry, async_get_device_automation_capabilities, diff --git a/tests/components/rest/test_binary_sensor.py b/tests/components/rest/test_binary_sensor.py index 2638477ef79..9adb04ea40c 100644 --- a/tests/components/rest/test_binary_sensor.py +++ b/tests/components/rest/test_binary_sensor.py @@ -2,6 +2,7 @@ import asyncio from os import path +from unittest.mock import patch import httpx import respx @@ -18,8 +19,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import patch - async def test_setup_missing_basic_config(hass): """Test setup with configuration missing required entries.""" diff --git a/tests/components/rest/test_notify.py b/tests/components/rest/test_notify.py index 49f3876b97c..aa3e40c2dd4 100644 --- a/tests/components/rest/test_notify.py +++ b/tests/components/rest/test_notify.py @@ -1,5 +1,6 @@ """The tests for the rest.notify platform.""" from os import path +from unittest.mock import patch from homeassistant import config as hass_config import homeassistant.components.notify as notify @@ -7,8 +8,6 @@ from homeassistant.components.rest import DOMAIN from homeassistant.const import SERVICE_RELOAD from homeassistant.setup import async_setup_component -from tests.async_mock import patch - async def test_reload_notify(hass): """Verify we can reload the notify service.""" diff --git a/tests/components/rest/test_sensor.py b/tests/components/rest/test_sensor.py index 16d3f8ba0ac..58309cd7532 100644 --- a/tests/components/rest/test_sensor.py +++ b/tests/components/rest/test_sensor.py @@ -1,6 +1,7 @@ """The tests for the REST sensor platform.""" import asyncio from os import path +from unittest.mock import patch import httpx import respx @@ -18,8 +19,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import patch - async def test_setup_missing_config(hass): """Test setup with configuration missing required entries.""" diff --git a/tests/components/rflink/test_binary_sensor.py b/tests/components/rflink/test_binary_sensor.py index e20d2554f97..118a3689fc7 100644 --- a/tests/components/rflink/test_binary_sensor.py +++ b/tests/components/rflink/test_binary_sensor.py @@ -5,6 +5,7 @@ Test setup of rflink sensor component/platform. Verify manual and automatic sensor creation. """ from datetime import timedelta +from unittest.mock import patch from homeassistant.components.rflink import CONF_RECONNECT_INTERVAL from homeassistant.const import ( @@ -16,7 +17,6 @@ from homeassistant.const import ( import homeassistant.core as ha import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.rflink.test_init import mock_rflink diff --git a/tests/components/rflink/test_init.py b/tests/components/rflink/test_init.py index 2ae3724ee66..7ba90286e62 100644 --- a/tests/components/rflink/test_init.py +++ b/tests/components/rflink/test_init.py @@ -1,5 +1,7 @@ """Common functions for RFLink component tests and generic platform tests.""" +from unittest.mock import Mock + import pytest from voluptuous.error import MultipleInvalid @@ -15,8 +17,6 @@ from homeassistant.components.rflink import ( ) from homeassistant.const import ATTR_ENTITY_ID, SERVICE_STOP_COVER, SERVICE_TURN_OFF -from tests.async_mock import Mock - async def mock_rflink( hass, config, domain, monkeypatch, failures=None, failcommand=False diff --git a/tests/components/rfxtrx/conftest.py b/tests/components/rfxtrx/conftest.py index 563fb362e5a..ee695bee9dd 100644 --- a/tests/components/rfxtrx/conftest.py +++ b/tests/components/rfxtrx/conftest.py @@ -1,5 +1,6 @@ """Common test tools.""" from datetime import timedelta +from unittest.mock import patch import pytest @@ -7,7 +8,6 @@ from homeassistant.components import rfxtrx from homeassistant.components.rfxtrx import DOMAIN from homeassistant.util.dt import utcnow -from tests.async_mock import patch from tests.common import MockConfigEntry, async_fire_time_changed from tests.components.light.conftest import mock_light_profiles # noqa diff --git a/tests/components/rfxtrx/test_config_flow.py b/tests/components/rfxtrx/test_config_flow.py index 04545a1a422..e39c766bfd2 100644 --- a/tests/components/rfxtrx/test_config_flow.py +++ b/tests/components/rfxtrx/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Tado config flow.""" import os +from unittest.mock import MagicMock, patch, sentinel import serial.tools.list_ports @@ -13,7 +14,6 @@ from homeassistant.helpers.entity_registry import ( async_get_registry as async_get_entity_registry, ) -from tests.async_mock import MagicMock, patch, sentinel from tests.common import MockConfigEntry @@ -1103,6 +1103,90 @@ async def test_options_add_and_configure_device(hass): assert "delay_off" not in entry.data["devices"]["0913000022670e013970"] +async def test_options_configure_rfy_cover_device(hass): + """Test we can configure the venetion blind mode of an Rfy cover.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry = MockConfigEntry( + domain=DOMAIN, + data={ + "host": None, + "port": None, + "device": "/dev/tty123", + "automatic_add": False, + "devices": {}, + }, + unique_id=DOMAIN, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "prompt_options" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "automatic_add": True, + "event_code": "071a000001020301", + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "set_device_options" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "fire_event": False, + "venetian_blind_mode": "EU", + }, + ) + + await hass.async_block_till_done() + + assert entry.data["devices"]["071a000001020301"]["venetian_blind_mode"] == "EU" + + device_registry = await async_get_device_registry(hass) + device_entries = async_entries_for_config_entry(device_registry, entry.entry_id) + + assert device_entries[0].id + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "prompt_options" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "automatic_add": False, + "device": device_entries[0].id, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "set_device_options" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "fire_event": False, + "venetian_blind_mode": "EU", + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + + assert entry.data["devices"]["071a000001020301"]["venetian_blind_mode"] == "EU" + + def test_get_serial_by_id_no_dir(): """Test serial by id conversion if there's no /dev/serial/by-id.""" p1 = patch("os.path.isdir", MagicMock(return_value=False)) diff --git a/tests/components/rfxtrx/test_cover.py b/tests/components/rfxtrx/test_cover.py index b3e5ce224c6..fe7f49d728b 100644 --- a/tests/components/rfxtrx/test_cover.py +++ b/tests/components/rfxtrx/test_cover.py @@ -140,3 +140,180 @@ async def test_duplicate_cover(hass, rfxtrx): assert state assert state.state == "closed" assert state.attributes.get("friendly_name") == "LightwaveRF, Siemens 0213c7:242" + + +async def test_rfy_cover(hass, rfxtrx): + """Test Rfy venetian blind covers.""" + entry_data = create_rfx_test_cfg( + devices={ + "071a000001020301": { + "signal_repetitions": 1, + "venetian_blind_mode": "Unknown", + }, + "071a000001020302": {"signal_repetitions": 1, "venetian_blind_mode": "US"}, + "071a000001020303": {"signal_repetitions": 1, "venetian_blind_mode": "EU"}, + } + ) + mock_entry = MockConfigEntry(domain="rfxtrx", unique_id=DOMAIN, data=entry_data) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) + await hass.async_block_till_done() + + # Test a blind with no venetian mode setting + state = hass.states.get("cover.rfy_010203_1") + assert state + + await hass.services.async_call( + "cover", + "stop_cover", + {"entity_id": "cover.rfy_010203_1"}, + blocking=True, + ) + + await hass.services.async_call( + "cover", + "open_cover", + {"entity_id": "cover.rfy_010203_1"}, + blocking=True, + ) + + await hass.services.async_call( + "cover", + "close_cover", + {"entity_id": "cover.rfy_010203_1"}, + blocking=True, + ) + + await hass.services.async_call( + "cover", + "open_cover_tilt", + {"entity_id": "cover.rfy_010203_1"}, + blocking=True, + ) + + await hass.services.async_call( + "cover", + "close_cover_tilt", + {"entity_id": "cover.rfy_010203_1"}, + blocking=True, + ) + + assert rfxtrx.transport.send.mock_calls == [ + call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x01\x00")), + call(bytearray(b"\x08\x1a\x00\x01\x01\x02\x03\x01\x01")), + call(bytearray(b"\x08\x1a\x00\x02\x01\x02\x03\x01\x03")), + ] + + # Test a blind with venetian mode set to US + state = hass.states.get("cover.rfy_010203_2") + assert state + rfxtrx.transport.send.mock_calls = [] + + await hass.services.async_call( + "cover", + "stop_cover", + {"entity_id": "cover.rfy_010203_2"}, + blocking=True, + ) + + await hass.services.async_call( + "cover", + "open_cover", + {"entity_id": "cover.rfy_010203_2"}, + blocking=True, + ) + + await hass.services.async_call( + "cover", + "close_cover", + {"entity_id": "cover.rfy_010203_2"}, + blocking=True, + ) + + await hass.services.async_call( + "cover", + "open_cover_tilt", + {"entity_id": "cover.rfy_010203_2"}, + blocking=True, + ) + + await hass.services.async_call( + "cover", + "close_cover_tilt", + {"entity_id": "cover.rfy_010203_2"}, + blocking=True, + ) + + await hass.services.async_call( + "cover", + "stop_cover_tilt", + {"entity_id": "cover.rfy_010203_2"}, + blocking=True, + ) + + assert rfxtrx.transport.send.mock_calls == [ + call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x02\x00")), + call(bytearray(b"\x08\x1a\x00\x01\x01\x02\x03\x02\x0F")), + call(bytearray(b"\x08\x1a\x00\x02\x01\x02\x03\x02\x10")), + call(bytearray(b"\x08\x1a\x00\x03\x01\x02\x03\x02\x11")), + call(bytearray(b"\x08\x1a\x00\x04\x01\x02\x03\x02\x12")), + call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x02\x00")), + ] + + # Test a blind with venetian mode set to EU + state = hass.states.get("cover.rfy_010203_3") + assert state + rfxtrx.transport.send.mock_calls = [] + + await hass.services.async_call( + "cover", + "stop_cover", + {"entity_id": "cover.rfy_010203_3"}, + blocking=True, + ) + + await hass.services.async_call( + "cover", + "open_cover", + {"entity_id": "cover.rfy_010203_3"}, + blocking=True, + ) + + await hass.services.async_call( + "cover", + "close_cover", + {"entity_id": "cover.rfy_010203_3"}, + blocking=True, + ) + + await hass.services.async_call( + "cover", + "open_cover_tilt", + {"entity_id": "cover.rfy_010203_3"}, + blocking=True, + ) + + await hass.services.async_call( + "cover", + "close_cover_tilt", + {"entity_id": "cover.rfy_010203_3"}, + blocking=True, + ) + + await hass.services.async_call( + "cover", + "stop_cover_tilt", + {"entity_id": "cover.rfy_010203_3"}, + blocking=True, + ) + + assert rfxtrx.transport.send.mock_calls == [ + call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x03\x00")), + call(bytearray(b"\x08\x1a\x00\x01\x01\x02\x03\x03\x11")), + call(bytearray(b"\x08\x1a\x00\x02\x01\x02\x03\x03\x12")), + call(bytearray(b"\x08\x1a\x00\x03\x01\x02\x03\x03\x0F")), + call(bytearray(b"\x08\x1a\x00\x04\x01\x02\x03\x03\x10")), + call(bytearray(b"\x08\x1a\x00\x00\x01\x02\x03\x03\x00")), + ] diff --git a/tests/components/rfxtrx/test_init.py b/tests/components/rfxtrx/test_init.py index 037b08b7cc6..9112082a956 100644 --- a/tests/components/rfxtrx/test_init.py +++ b/tests/components/rfxtrx/test_init.py @@ -1,11 +1,16 @@ """The tests for the Rfxtrx component.""" +from unittest.mock import call + from homeassistant.components.rfxtrx import DOMAIN from homeassistant.components.rfxtrx.const import EVENT_RFXTRX_EVENT from homeassistant.core import callback +from homeassistant.helpers.device_registry import ( + DeviceRegistry, + async_get_registry as async_get_device_registry, +) from homeassistant.setup import async_setup_component -from tests.async_mock import call from tests.common import MockConfigEntry from tests.components.rfxtrx.conftest import create_rfx_test_cfg @@ -74,6 +79,8 @@ async def test_fire_event(hass, rfxtrx): await hass.async_block_till_done() await hass.async_start() + device_registry: DeviceRegistry = await async_get_device_registry(hass) + calls = [] @callback @@ -87,6 +94,16 @@ async def test_fire_event(hass, rfxtrx): await rfxtrx.signal("0b1100cd0213c7f210010f51") await rfxtrx.signal("0716000100900970") + device_id_1 = device_registry.async_get_device( + identifiers={("rfxtrx", "11", "0", "213c7f2:16")} + ) + assert device_id_1 + + device_id_2 = device_registry.async_get_device( + identifiers={("rfxtrx", "16", "0", "00:90")} + ) + assert device_id_2 + assert calls == [ { "packet_type": 17, @@ -95,6 +112,7 @@ async def test_fire_event(hass, rfxtrx): "id_string": "213c7f2:16", "data": "0b1100cd0213c7f210010f51", "values": {"Command": "On", "Rssi numeric": 5}, + "device_id": device_id_1.id, }, { "packet_type": 22, @@ -103,6 +121,7 @@ async def test_fire_event(hass, rfxtrx): "id_string": "00:90", "data": "0716000100900970", "values": {"Command": "Chime", "Rssi numeric": 7, "Sound": 9}, + "device_id": device_id_2.id, }, ] diff --git a/tests/components/ring/common.py b/tests/components/ring/common.py index 39b5c339677..93a6e4f91e0 100644 --- a/tests/components/ring/common.py +++ b/tests/components/ring/common.py @@ -1,8 +1,9 @@ """Common methods used across the tests for ring devices.""" +from unittest.mock import patch + from homeassistant.components.ring import DOMAIN from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/ring/test_binary_sensor.py b/tests/components/ring/test_binary_sensor.py index 8edaf2c229b..0b73c739503 100644 --- a/tests/components/ring/test_binary_sensor.py +++ b/tests/components/ring/test_binary_sensor.py @@ -1,10 +1,9 @@ """The tests for the Ring binary sensor platform.""" from time import time +from unittest.mock import patch from .common import setup_platform -from tests.async_mock import patch - async def test_binary_sensor(hass, requests_mock): """Test the Ring binary sensors.""" diff --git a/tests/components/ring/test_config_flow.py b/tests/components/ring/test_config_flow.py index fc2b490f560..85ca4ffb558 100644 --- a/tests/components/ring/test_config_flow.py +++ b/tests/components/ring/test_config_flow.py @@ -1,10 +1,10 @@ """Test the Ring config flow.""" +from unittest.mock import Mock, patch + from homeassistant import config_entries, setup from homeassistant.components.ring import DOMAIN from homeassistant.components.ring.config_flow import InvalidAuth -from tests.async_mock import Mock, patch - async def test_form(hass): """Test we get the form.""" diff --git a/tests/components/risco/test_alarm_control_panel.py b/tests/components/risco/test_alarm_control_panel.py index bf7c971df54..a5a16379fe9 100644 --- a/tests/components/risco/test_alarm_control_panel.py +++ b/tests/components/risco/test_alarm_control_panel.py @@ -1,4 +1,6 @@ """Tests for the Risco alarm control panel device.""" +from unittest.mock import MagicMock, PropertyMock, patch + import pytest from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN @@ -29,7 +31,6 @@ from homeassistant.helpers.entity_component import async_update_entity from .util import TEST_CONFIG, TEST_SITE_UUID, setup_risco -from tests.async_mock import MagicMock, PropertyMock, patch from tests.common import MockConfigEntry FIRST_ENTITY_ID = "alarm_control_panel.risco_test_site_name_partition_0" @@ -147,11 +148,11 @@ async def test_setup(hass, two_part_alarm): assert registry.async_is_registered(SECOND_ENTITY_ID) registry = await hass.helpers.device_registry.async_get_registry() - device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_0")}, {}) + device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_0")}) assert device is not None assert device.manufacturer == "Risco" - device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_1")}, {}) + device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_1")}) assert device is not None assert device.manufacturer == "Risco" @@ -166,7 +167,7 @@ async def _check_state(hass, alarm, property, state, entity_id, partition_id): async def test_states(hass, two_part_alarm): """Test the various alarm states.""" - await setup_risco(hass, CUSTOM_MAPPING_OPTIONS) + await setup_risco(hass, [], CUSTOM_MAPPING_OPTIONS) assert hass.states.get(FIRST_ENTITY_ID).state == STATE_UNKNOWN for partition_id, entity_id in {0: FIRST_ENTITY_ID, 1: SECOND_ENTITY_ID}.items(): @@ -248,7 +249,7 @@ async def _call_alarm_service(hass, service, entity_id, **kwargs): async def test_sets_custom_mapping(hass, two_part_alarm): """Test settings the various modes when mapping some states.""" - await setup_risco(hass, CUSTOM_MAPPING_OPTIONS) + await setup_risco(hass, [], CUSTOM_MAPPING_OPTIONS) registry = await hass.helpers.entity_registry.async_get_registry() entity = registry.async_get(FIRST_ENTITY_ID) @@ -274,7 +275,7 @@ async def test_sets_custom_mapping(hass, two_part_alarm): async def test_sets_full_custom_mapping(hass, two_part_alarm): """Test settings the various modes when mapping all states.""" - await setup_risco(hass, FULL_CUSTOM_MAPPING) + await setup_risco(hass, [], FULL_CUSTOM_MAPPING) registry = await hass.helpers.entity_registry.async_get_registry() entity = registry.async_get(FIRST_ENTITY_ID) @@ -308,7 +309,7 @@ async def test_sets_full_custom_mapping(hass, two_part_alarm): async def test_sets_with_correct_code(hass, two_part_alarm): """Test settings the various modes when code is required.""" - await setup_risco(hass, {**CUSTOM_MAPPING_OPTIONS, **CODES_REQUIRED_OPTIONS}) + await setup_risco(hass, [], {**CUSTOM_MAPPING_OPTIONS, **CODES_REQUIRED_OPTIONS}) code = {"code": 1234} await _test_service_call( @@ -350,7 +351,7 @@ async def test_sets_with_correct_code(hass, two_part_alarm): async def test_sets_with_incorrect_code(hass, two_part_alarm): """Test settings the various modes when code is required and incorrect.""" - await setup_risco(hass, {**CUSTOM_MAPPING_OPTIONS, **CODES_REQUIRED_OPTIONS}) + await setup_risco(hass, [], {**CUSTOM_MAPPING_OPTIONS, **CODES_REQUIRED_OPTIONS}) code = {"code": 4321} await _test_no_service_call( diff --git a/tests/components/risco/test_binary_sensor.py b/tests/components/risco/test_binary_sensor.py index 50b9c43c5c3..7533512e3ef 100644 --- a/tests/components/risco/test_binary_sensor.py +++ b/tests/components/risco/test_binary_sensor.py @@ -1,4 +1,6 @@ """Tests for the Risco binary sensors.""" +from unittest.mock import PropertyMock, patch + from homeassistant.components.risco import CannotConnectError, UnauthorizedError from homeassistant.components.risco.const import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON @@ -7,7 +9,6 @@ from homeassistant.helpers.entity_component import async_update_entity from .util import TEST_CONFIG, TEST_SITE_UUID, setup_risco from .util import two_zone_alarm # noqa: F401 -from tests.async_mock import PropertyMock, patch from tests.common import MockConfigEntry FIRST_ENTITY_ID = "binary_sensor.zone_0" @@ -59,11 +60,11 @@ async def test_setup(hass, two_zone_alarm): # noqa: F811 assert registry.async_is_registered(SECOND_ENTITY_ID) registry = await hass.helpers.device_registry.async_get_registry() - device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_zone_0")}, {}) + device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_zone_0")}) assert device is not None assert device.manufacturer == "Risco" - device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_zone_1")}, {}) + device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_zone_1")}) assert device is not None assert device.manufacturer == "Risco" diff --git a/tests/components/risco/test_config_flow.py b/tests/components/risco/test_config_flow.py index ba14a52553e..cfb1a410960 100644 --- a/tests/components/risco/test_config_flow.py +++ b/tests/components/risco/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Risco config flow.""" +from unittest.mock import PropertyMock, patch + import pytest import voluptuous as vol @@ -9,7 +11,6 @@ from homeassistant.components.risco.config_flow import ( ) from homeassistant.components.risco.const import DOMAIN -from tests.async_mock import PropertyMock, patch from tests.common import MockConfigEntry TEST_SITE_NAME = "test-site-name" diff --git a/tests/components/risco/test_sensor.py b/tests/components/risco/test_sensor.py index acb92088478..3d449f10e46 100644 --- a/tests/components/risco/test_sensor.py +++ b/tests/components/risco/test_sensor.py @@ -1,16 +1,19 @@ """Tests for the Risco event sensors.""" +from datetime import timedelta +from unittest.mock import MagicMock, PropertyMock, patch + from homeassistant.components.risco import ( LAST_EVENT_TIMESTAMP_KEY, CannotConnectError, UnauthorizedError, ) -from homeassistant.components.risco.const import DOMAIN, EVENTS_COORDINATOR +from homeassistant.components.risco.const import DOMAIN +from homeassistant.util import dt -from .util import TEST_CONFIG, setup_risco +from .util import TEST_CONFIG, TEST_SITE_UUID, setup_risco from .util import two_zone_alarm # noqa: F401 -from tests.async_mock import MagicMock, patch -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed ENTITY_IDS = { "Alarm": "sensor.risco_test_site_name_alarm_events", @@ -170,31 +173,28 @@ async def test_setup(hass, two_zone_alarm): # noqa: F811 assert not registry.async_is_registered(id) with patch( - "homeassistant.components.risco.RiscoAPI.get_events", - return_value=TEST_EVENTS, + "homeassistant.components.risco.RiscoAPI.site_uuid", + new_callable=PropertyMock(return_value=TEST_SITE_UUID), ), patch( "homeassistant.components.risco.Store.async_save", ) as save_mock: - entry = await setup_risco(hass) - await hass.async_block_till_done() + await setup_risco(hass, TEST_EVENTS) + for id in ENTITY_IDS.values(): + assert registry.async_is_registered(id) + save_mock.assert_awaited_once_with( {LAST_EVENT_TIMESTAMP_KEY: TEST_EVENTS[0].time} ) + for category, entity_id in ENTITY_IDS.items(): + _check_state(hass, category, entity_id) - for id in ENTITY_IDS.values(): - assert registry.async_is_registered(id) - - for category, entity_id in ENTITY_IDS.items(): - _check_state(hass, category, entity_id) - - coordinator = hass.data[DOMAIN][entry.entry_id][EVENTS_COORDINATOR] with patch( "homeassistant.components.risco.RiscoAPI.get_events", return_value=[] ) as events_mock, patch( "homeassistant.components.risco.Store.async_load", return_value={LAST_EVENT_TIMESTAMP_KEY: TEST_EVENTS[0].time}, ): - await coordinator.async_refresh() + async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=65)) await hass.async_block_till_done() events_mock.assert_awaited_once_with(TEST_EVENTS[0].time, 10) diff --git a/tests/components/risco/util.py b/tests/components/risco/util.py index 704c12fb846..8b918f32c12 100644 --- a/tests/components/risco/util.py +++ b/tests/components/risco/util.py @@ -1,10 +1,11 @@ """Utilities for Risco tests.""" +from unittest.mock import MagicMock, PropertyMock, patch + from pytest import fixture from homeassistant.components.risco.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_PIN, CONF_USERNAME -from tests.async_mock import MagicMock, PropertyMock, patch from tests.common import MockConfigEntry TEST_CONFIG = { @@ -16,7 +17,7 @@ TEST_SITE_UUID = "test-site-uuid" TEST_SITE_NAME = "test-site-name" -async def setup_risco(hass, options={}): +async def setup_risco(hass, events=[], options={}): """Set up a Risco integration for testing.""" config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG, options=options) config_entry.add_to_hass(hass) @@ -32,6 +33,9 @@ async def setup_risco(hass, options={}): new_callable=PropertyMock(return_value=TEST_SITE_NAME), ), patch( "homeassistant.components.risco.RiscoAPI.close" + ), patch( + "homeassistant.components.risco.RiscoAPI.get_events", + return_value=events, ): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/rmvtransport/test_sensor.py b/tests/components/rmvtransport/test_sensor.py index 935368f03b1..a1022d2f4b0 100644 --- a/tests/components/rmvtransport/test_sensor.py +++ b/tests/components/rmvtransport/test_sensor.py @@ -1,10 +1,9 @@ """The tests for the rmvtransport platform.""" import datetime +from unittest.mock import patch from homeassistant.setup import async_setup_component -from tests.async_mock import patch - VALID_CONFIG_MINIMAL = { "sensor": {"platform": "rmvtransport", "next_departure": [{"station": "3000010"}]} } diff --git a/tests/components/roku/test_config_flow.py b/tests/components/roku/test_config_flow.py index 16e4a434dc3..ed1b042e328 100644 --- a/tests/components/roku/test_config_flow.py +++ b/tests/components/roku/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Roku config flow.""" +from unittest.mock import patch + from homeassistant.components.roku.const import DOMAIN from homeassistant.config_entries import SOURCE_HOMEKIT, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE @@ -10,7 +12,6 @@ from homeassistant.data_entry_flow import ( from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.components.roku import ( HOMEKIT_HOST, HOST, diff --git a/tests/components/roku/test_init.py b/tests/components/roku/test_init.py index b929a48ee25..a5f16c6071f 100644 --- a/tests/components/roku/test_init.py +++ b/tests/components/roku/test_init.py @@ -1,4 +1,6 @@ """Tests for the Roku integration.""" +from unittest.mock import patch + from homeassistant.components.roku.const import DOMAIN from homeassistant.config_entries import ( ENTRY_STATE_LOADED, @@ -7,7 +9,6 @@ from homeassistant.config_entries import ( ) from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import patch from tests.components.roku import setup_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/roku/test_media_player.py b/tests/components/roku/test_media_player.py index 23dd9dbc6c8..1a1b46117bd 100644 --- a/tests/components/roku/test_media_player.py +++ b/tests/components/roku/test_media_player.py @@ -1,5 +1,6 @@ """Tests for the Roku Media Player platform.""" from datetime import timedelta +from unittest.mock import patch from rokuecp import RokuError @@ -39,6 +40,7 @@ from homeassistant.components.media_player.const import ( ) from homeassistant.components.roku.const import ATTR_KEYWORD, DOMAIN, SERVICE_SEARCH from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.config import async_process_ha_core_config from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_MEDIA_NEXT_TRACK, @@ -62,7 +64,6 @@ from homeassistant.const import ( from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import dt as dt_util -from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.roku import UPNP_SERIAL, setup_integration from tests.test_util.aiohttp import AiohttpClientMocker @@ -599,6 +600,68 @@ async def test_media_browse(hass, aioclient_mock, hass_ws_client): assert not msg["success"] +async def test_media_browse_internal(hass, aioclient_mock, hass_ws_client): + """Test browsing media with internal url.""" + await async_process_ha_core_config( + hass, + {"internal_url": "http://example.local:8123"}, + ) + + assert hass.config.internal_url == "http://example.local:8123" + + await setup_integration( + hass, + aioclient_mock, + device="rokutv", + app="tvinput-dtv", + host=TV_HOST, + unique_id=TV_SERIAL, + ) + + client = await hass_ws_client(hass) + + with patch( + "homeassistant.helpers.network._get_request_host", return_value="example.local" + ): + await client.send_json( + { + "id": 2, + "type": "media_player/browse_media", + "entity_id": TV_ENTITY_ID, + "media_content_type": MEDIA_TYPE_APPS, + "media_content_id": "apps", + } + ) + + msg = await client.receive_json() + + assert msg["id"] == 2 + assert msg["type"] == TYPE_RESULT + assert msg["success"] + + assert msg["result"] + assert msg["result"]["title"] == "Apps" + assert msg["result"]["media_class"] == MEDIA_CLASS_DIRECTORY + assert msg["result"]["media_content_type"] == MEDIA_TYPE_APPS + assert msg["result"]["children_media_class"] == MEDIA_CLASS_APP + assert msg["result"]["can_expand"] + assert not msg["result"]["can_play"] + assert len(msg["result"]["children"]) == 11 + assert msg["result"]["children_media_class"] == MEDIA_CLASS_APP + + assert msg["result"]["children"][0]["title"] == "Satellite TV" + assert msg["result"]["children"][0]["media_content_type"] == MEDIA_TYPE_APP + assert msg["result"]["children"][0]["media_content_id"] == "tvinput.hdmi2" + assert "/query/icon/tvinput.hdmi2" in msg["result"]["children"][0]["thumbnail"] + assert msg["result"]["children"][0]["can_play"] + + assert msg["result"]["children"][3]["title"] == "Roku Channel Store" + assert msg["result"]["children"][3]["media_content_type"] == MEDIA_TYPE_APP + assert msg["result"]["children"][3]["media_content_id"] == "11" + assert "/query/icon/11" in msg["result"]["children"][3]["thumbnail"] + assert msg["result"]["children"][3]["can_play"] + + async def test_integration_services( hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker ) -> None: diff --git a/tests/components/roku/test_remote.py b/tests/components/roku/test_remote.py index 96426e5b10a..4122e0af1d1 100644 --- a/tests/components/roku/test_remote.py +++ b/tests/components/roku/test_remote.py @@ -1,4 +1,6 @@ """The tests for the Roku remote platform.""" +from unittest.mock import patch + from homeassistant.components.remote import ( ATTR_COMMAND, DOMAIN as REMOTE_DOMAIN, @@ -7,7 +9,6 @@ from homeassistant.components.remote import ( from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_OFF, SERVICE_TURN_ON from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import patch from tests.components.roku import UPNP_SERIAL, setup_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/roomba/test_config_flow.py b/tests/components/roomba/test_config_flow.py index 253250d7d49..bf8e674950f 100644 --- a/tests/components/roomba/test_config_flow.py +++ b/tests/components/roomba/test_config_flow.py @@ -1,7 +1,11 @@ """Test the iRobot Roomba config flow.""" +from unittest.mock import MagicMock, PropertyMock, patch + from roombapy import RoombaConnectionError +from roombapy.roomba import RoombaInfo from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.components.roomba.const import ( CONF_BLID, CONF_CONTINUOUS, @@ -10,19 +14,11 @@ from homeassistant.components.roomba.const import ( ) from homeassistant.const import CONF_HOST, CONF_PASSWORD -from tests.async_mock import MagicMock, PropertyMock, patch from tests.common import MockConfigEntry +MOCK_IP = "1.2.3.4" VALID_CONFIG = {CONF_HOST: "1.2.3.4", CONF_BLID: "blid", CONF_PASSWORD: "password"} -VALID_YAML_CONFIG = { - CONF_HOST: "1.2.3.4", - CONF_BLID: "blid", - CONF_PASSWORD: "password", - CONF_CONTINUOUS: True, - CONF_DELAY: 1, -} - def _create_mocked_roomba( roomba_connected=None, master_state=None, connect=None, disconnect=None @@ -35,55 +31,233 @@ def _create_mocked_roomba( return mocked_roomba -async def test_form(hass): - """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} +def _mocked_discovery(*_): + roomba_discovery = MagicMock() + + roomba = RoombaInfo( + hostname="iRobot-blid", + robot_name="robot_name", + ip=MOCK_IP, + mac="mac", + firmware="firmware", + sku="sku", + capabilities="capabilities", ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {} + + roomba_discovery.get_all = MagicMock(return_value=[roomba]) + return roomba_discovery + + +def _mocked_failed_discovery(*_): + roomba_discovery = MagicMock() + roomba_discovery.get_all = MagicMock(return_value=[]) + return roomba_discovery + + +def _mocked_getpassword(*_): + roomba_password = MagicMock() + roomba_password.get_password = MagicMock(return_value="password") + return roomba_password + + +def _mocked_failed_getpassword(*_): + roomba_password = MagicMock() + roomba_password.get_password = MagicMock(return_value=None) + return roomba_password + + +def _mocked_connection_refused_on_getpassword(*_): + roomba_password = MagicMock() + roomba_password.get_password = MagicMock(side_effect=ConnectionRefusedError) + return roomba_password + + +async def test_form_user_discovery_and_password_fetch(hass): + """Test we can discovery and fetch the password.""" + await setup.async_setup_component(hass, "persistent_notification", {}) mocked_roomba = _create_mocked_roomba( roomba_connected=True, master_state={"state": {"reported": {"name": "myroomba"}}}, ) + with patch( + "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "user" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: MOCK_IP}, + ) + await hass.async_block_till_done() + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] is None + assert result2["step_id"] == "link" + with patch( "homeassistant.components.roomba.config_flow.Roomba", return_value=mocked_roomba, + ), patch( + "homeassistant.components.roomba.config_flow.RoombaPassword", + _mocked_getpassword, ), patch( "homeassistant.components.roomba.async_setup", return_value=True ) as mock_setup, patch( "homeassistant.components.roomba.async_setup_entry", return_value=True, ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - VALID_CONFIG, + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {}, ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "myroomba" - - assert result2["result"].unique_id == "blid" - assert result2["data"] == { + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["title"] == "robot_name" + assert result3["result"].unique_id == "blid" + assert result3["data"] == { CONF_BLID: "blid", CONF_CONTINUOUS: True, CONF_DELAY: 1, - CONF_HOST: "1.2.3.4", + CONF_HOST: MOCK_IP, CONF_PASSWORD: "password", } assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_cannot_connect(hass): - """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} +async def test_form_user_discovery_skips_known(hass): + """Test discovery proceeds to manual if all discovered are already known.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry = MockConfigEntry(domain=DOMAIN, data=VALID_CONFIG, unique_id="blid") + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "manual" + + +async def test_form_user_failed_discovery_aborts_already_configured(hass): + """Test if we manually configure an existing host we abort.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry = MockConfigEntry(domain=DOMAIN, data=VALID_CONFIG, unique_id="blid") + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.roomba.config_flow.RoombaDiscovery", + _mocked_failed_discovery, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "manual" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: MOCK_IP, CONF_BLID: "blid"}, ) + await hass.async_block_till_done() + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "already_configured" + + +async def test_form_user_discovery_manual_and_auto_password_fetch(hass): + """Test discovery skipped and we can auto fetch the password.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mocked_roomba = _create_mocked_roomba( + roomba_connected=True, + master_state={"state": {"reported": {"name": "myroomba"}}}, + ) + + with patch( + "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "user" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: None}, + ) + await hass.async_block_till_done() + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] is None + assert result2["step_id"] == "manual" + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {CONF_HOST: MOCK_IP, CONF_BLID: "blid"}, + ) + await hass.async_block_till_done() + assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["errors"] is None + + with patch( + "homeassistant.components.roomba.config_flow.Roomba", + return_value=mocked_roomba, + ), patch( + "homeassistant.components.roomba.config_flow.RoombaPassword", + _mocked_getpassword, + ), patch( + "homeassistant.components.roomba.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.roomba.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["title"] == "myroomba" + assert result4["result"].unique_id == "blid" + assert result4["data"] == { + CONF_BLID: "blid", + CONF_CONTINUOUS: True, + CONF_DELAY: 1, + CONF_HOST: MOCK_IP, + CONF_PASSWORD: "password", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_user_discovery_manual_and_auto_password_fetch_but_cannot_connect( + hass, +): + """Test discovery skipped and we can auto fetch the password then we fail to connect.""" + await setup.async_setup_component(hass, "persistent_notification", {}) mocked_roomba = _create_mocked_roomba( connect=RoombaConnectionError, @@ -91,27 +265,161 @@ async def test_form_cannot_connect(hass): master_state={"state": {"reported": {"name": "myroomba"}}}, ) + with patch( + "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "user" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: None}, + ) + await hass.async_block_till_done() + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] is None + assert result2["step_id"] == "manual" + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {CONF_HOST: MOCK_IP, CONF_BLID: "blid"}, + ) + await hass.async_block_till_done() + assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["errors"] is None + with patch( "homeassistant.components.roomba.config_flow.Roomba", return_value=mocked_roomba, - ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - VALID_CONFIG, + ), patch( + "homeassistant.components.roomba.config_flow.RoombaPassword", + _mocked_getpassword, + ), patch( + "homeassistant.components.roomba.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.roomba.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + {}, ) + await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["errors"] == {"base": "cannot_connect"} + assert result4["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result4["reason"] == "cannot_connect" + assert len(mock_setup.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 -async def test_form_import(hass): - """Test we can import yaml config.""" +async def test_form_user_discovery_fails_and_auto_password_fetch(hass): + """Test discovery fails and we can auto fetch the password.""" + await setup.async_setup_component(hass, "persistent_notification", {}) mocked_roomba = _create_mocked_roomba( roomba_connected=True, - master_state={"state": {"reported": {"name": "imported_roomba"}}}, + master_state={"state": {"reported": {"name": "myroomba"}}}, ) + with patch( + "homeassistant.components.roomba.config_flow.RoombaDiscovery", + _mocked_failed_discovery, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "manual" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: MOCK_IP, CONF_BLID: "blid"}, + ) + await hass.async_block_till_done() + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] is None + + with patch( + "homeassistant.components.roomba.config_flow.Roomba", + return_value=mocked_roomba, + ), patch( + "homeassistant.components.roomba.config_flow.RoombaPassword", + _mocked_getpassword, + ), patch( + "homeassistant.components.roomba.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.roomba.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3["title"] == "myroomba" + assert result3["result"].unique_id == "blid" + assert result3["data"] == { + CONF_BLID: "blid", + CONF_CONTINUOUS: True, + CONF_DELAY: 1, + CONF_HOST: MOCK_IP, + CONF_PASSWORD: "password", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_user_discovery_fails_and_password_fetch_fails(hass): + """Test discovery fails and password fetch fails.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mocked_roomba = _create_mocked_roomba( + roomba_connected=True, + master_state={"state": {"reported": {"name": "myroomba"}}}, + ) + + with patch( + "homeassistant.components.roomba.config_flow.RoombaDiscovery", + _mocked_failed_discovery, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "manual" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: MOCK_IP, CONF_BLID: "blid"}, + ) + await hass.async_block_till_done() + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] is None + + with patch( + "homeassistant.components.roomba.config_flow.RoombaPassword", + _mocked_failed_getpassword, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {}, + ) + await hass.async_block_till_done() + with patch( "homeassistant.components.roomba.config_flow.Roomba", return_value=mocked_roomba, @@ -121,39 +429,393 @@ async def test_form_import(hass): "homeassistant.components.roomba.async_setup_entry", return_value=True, ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=VALID_YAML_CONFIG.copy(), + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + {CONF_PASSWORD: "password"}, ) await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["result"].unique_id == "blid" - assert result["title"] == "imported_roomba" - assert result["data"] == { + assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["title"] == "myroomba" + assert result4["result"].unique_id == "blid" + assert result4["data"] == { CONF_BLID: "blid", CONF_CONTINUOUS: True, CONF_DELAY: 1, - CONF_HOST: "1.2.3.4", + CONF_HOST: MOCK_IP, CONF_PASSWORD: "password", } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_import_dupe(hass): - """Test we get abort on duplicate import.""" +async def test_form_user_discovery_fails_and_password_fetch_fails_and_cannot_connect( + hass, +): + """Test discovery fails and password fetch fails then we cannot connect.""" await setup.async_setup_component(hass, "persistent_notification", {}) - entry = MockConfigEntry(domain=DOMAIN, data=VALID_CONFIG, unique_id="blid") - entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=VALID_YAML_CONFIG.copy(), + mocked_roomba = _create_mocked_roomba( + connect=RoombaConnectionError, + roomba_connected=True, + master_state={"state": {"reported": {"name": "myroomba"}}}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + with patch( + "homeassistant.components.roomba.config_flow.RoombaDiscovery", + _mocked_failed_discovery, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "manual" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: MOCK_IP, CONF_BLID: "blid"}, + ) + await hass.async_block_till_done() + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] is None + + with patch( + "homeassistant.components.roomba.config_flow.RoombaPassword", + _mocked_failed_getpassword, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {}, + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.roomba.config_flow.Roomba", + return_value=mocked_roomba, + ), patch( + "homeassistant.components.roomba.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.roomba.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + {CONF_PASSWORD: "password"}, + ) + await hass.async_block_till_done() + + assert result4["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result4["errors"] == {"base": "cannot_connect"} + assert len(mock_setup.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_form_user_discovery_and_password_fetch_gets_connection_refused(hass): + """Test we can discovery and fetch the password manually.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mocked_roomba = _create_mocked_roomba( + roomba_connected=True, + master_state={"state": {"reported": {"name": "myroomba"}}}, + ) + + with patch( + "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "user" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: MOCK_IP}, + ) + await hass.async_block_till_done() + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] is None + assert result2["step_id"] == "link" + + with patch( + "homeassistant.components.roomba.config_flow.RoombaPassword", + _mocked_connection_refused_on_getpassword, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {}, + ) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.roomba.config_flow.Roomba", + return_value=mocked_roomba, + ), patch( + "homeassistant.components.roomba.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.roomba.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + {CONF_PASSWORD: "password"}, + ) + await hass.async_block_till_done() + + assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["title"] == "myroomba" + assert result4["result"].unique_id == "blid" + assert result4["data"] == { + CONF_BLID: "blid", + CONF_CONTINUOUS: True, + CONF_DELAY: 1, + CONF_HOST: MOCK_IP, + CONF_PASSWORD: "password", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_dhcp_discovery_and_roomba_discovery_finds(hass): + """Test we can process the discovery from dhcp and roomba discovery matches the device.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mocked_roomba = _create_mocked_roomba( + roomba_connected=True, + master_state={"state": {"reported": {"name": "myroomba"}}}, + ) + + with patch( + "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={ + IP_ADDRESS: MOCK_IP, + MAC_ADDRESS: "AA:BB:CC:DD:EE:FF", + HOSTNAME: "iRobot-blid", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "link" + assert result["description_placeholders"] == {"name": "robot_name"} + + with patch( + "homeassistant.components.roomba.config_flow.Roomba", + return_value=mocked_roomba, + ), patch( + "homeassistant.components.roomba.config_flow.RoombaPassword", + _mocked_getpassword, + ), patch( + "homeassistant.components.roomba.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.roomba.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "robot_name" + assert result2["result"].unique_id == "blid" + assert result2["data"] == { + CONF_BLID: "blid", + CONF_CONTINUOUS: True, + CONF_DELAY: 1, + CONF_HOST: MOCK_IP, + CONF_PASSWORD: "password", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_dhcp_discovery_falls_back_to_manual(hass): + """Test we can process the discovery from dhcp but roomba discovery cannot find the device.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mocked_roomba = _create_mocked_roomba( + roomba_connected=True, + master_state={"state": {"reported": {"name": "myroomba"}}}, + ) + + with patch( + "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={ + IP_ADDRESS: "1.1.1.1", + MAC_ADDRESS: "AA:BB:CC:DD:EE:FF", + HOSTNAME: "iRobot-blid", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + assert result["step_id"] == "user" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] is None + assert result2["step_id"] == "manual" + + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {CONF_HOST: "1.1.1.1", CONF_BLID: "blid"}, + ) + await hass.async_block_till_done() + assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result3["errors"] is None + + with patch( + "homeassistant.components.roomba.config_flow.Roomba", + return_value=mocked_roomba, + ), patch( + "homeassistant.components.roomba.config_flow.RoombaPassword", + _mocked_getpassword, + ), patch( + "homeassistant.components.roomba.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.roomba.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result4["title"] == "myroomba" + assert result4["result"].unique_id == "blid" + assert result4["data"] == { + CONF_BLID: "blid", + CONF_CONTINUOUS: True, + CONF_DELAY: 1, + CONF_HOST: "1.1.1.1", + CONF_PASSWORD: "password", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_dhcp_discovery_with_ignored(hass): + """Test ignored entries do not break checking for existing entries.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + config_entry = MockConfigEntry(domain=DOMAIN, data={}, source="ignore") + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={ + IP_ADDRESS: "1.1.1.1", + MAC_ADDRESS: "AA:BB:CC:DD:EE:FF", + HOSTNAME: "iRobot-blid", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "form" + + +async def test_dhcp_discovery_already_configured_host(hass): + """Test we abort if the host is already configured.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + config_entry = MockConfigEntry(domain=DOMAIN, data={CONF_HOST: "1.1.1.1"}) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={ + IP_ADDRESS: "1.1.1.1", + MAC_ADDRESS: "AA:BB:CC:DD:EE:FF", + HOSTNAME: "iRobot-blid", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" assert result["reason"] == "already_configured" + + +async def test_dhcp_discovery_already_configured_blid(hass): + """Test we abort if the blid is already configured.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_BLID: "blid"}, unique_id="blid" + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={ + IP_ADDRESS: "1.1.1.1", + MAC_ADDRESS: "AA:BB:CC:DD:EE:FF", + HOSTNAME: "iRobot-blid", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_dhcp_discovery_not_irobot(hass): + """Test we abort if the discovered device is not an irobot device.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + config_entry = MockConfigEntry( + domain=DOMAIN, data={CONF_BLID: "blid"}, unique_id="blid" + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.roomba.config_flow.RoombaDiscovery", _mocked_discovery + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={ + IP_ADDRESS: "1.1.1.1", + MAC_ADDRESS: "AA:BB:CC:DD:EE:FF", + HOSTNAME: "NotiRobot-blid", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == "not_irobot_device" diff --git a/tests/components/roon/test_config_flow.py b/tests/components/roon/test_config_flow.py index 7ffac08d9f6..4b4daab088d 100644 --- a/tests/components/roon/test_config_flow.py +++ b/tests/components/roon/test_config_flow.py @@ -1,50 +1,133 @@ """Test the roon config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, setup from homeassistant.components.roon.const import DOMAIN -from homeassistant.const import CONF_HOST - -from tests.async_mock import patch -from tests.common import MockConfigEntry class RoonApiMock: - """Mock to handle returning tokens for testing the RoonApi.""" - - def __init__(self, token): - """Initialize.""" - self._token = token + """Class to mock the Roon API for testing.""" @property def token(self): - """Return the auth token from the api.""" - return self._token + """Return a good authentication key.""" + return "good_token" - def stop(self): # pylint: disable=no-self-use - """Close down the api.""" + @property + def core_id(self): + """Return the roon host.""" + return "core_id" + + def stop(self): + """Stop socket and discovery.""" return -async def test_form_and_auth(hass): - """Test we get the form.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == "form" - assert result["errors"] == {} +class RoonApiMockNoToken(RoonApiMock): + """Class to mock the Roon API for testing, with failed authorisation.""" - with patch("homeassistant.components.roon.config_flow.TIMEOUT", 0,), patch( - "homeassistant.components.roon.const.AUTHENTICATE_TIMEOUT", - 0, - ), patch( + @property + def token(self): + """Return a bad authentication key.""" + return None + + +class RoonApiMockException(RoonApiMock): + """Class to mock the Roon API for testing, throws an unexpected exception.""" + + @property + def token(self): + """Throw exception.""" + raise Exception + + +class RoonDiscoveryMock: + """Class to mock Roon Discovery for testing.""" + + def all(self): + """Return a discovered roon server.""" + return ["2.2.2.2"] + + def stop(self): + """Stop discovery running.""" + return + + +class RoonDiscoveryFailedMock(RoonDiscoveryMock): + """Class to mock Roon Discovery for testing, with no servers discovered.""" + + def all(self): + """Return no discovered roon servers.""" + return [] + + +async def test_successful_discovery_and_auth(hass): + """Test when discovery and auth both work ok.""" + + await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( "homeassistant.components.roon.config_flow.RoonApi", - return_value=RoonApiMock("good_token"), + return_value=RoonApiMock(), + ), patch( + "homeassistant.components.roon.config_flow.RoonDiscovery", + return_value=RoonDiscoveryMock(), ), patch( "homeassistant.components.roon.async_setup", return_value=True - ) as mock_setup, patch( + ), patch( "homeassistant.components.roon.async_setup_entry", return_value=True, - ) as mock_setup_entry: + ): + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + # Should go straight to link if server was discovered + assert result["type"] == "form" + assert result["step_id"] == "link" + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + await hass.async_block_till_done() + + assert result2["title"] == "Roon Labs Music Player" + assert result2["data"] == { + "host": None, + "api_key": "good_token", + "roon_server_id": "core_id", + } + + +async def test_unsuccessful_discovery_user_form_and_auth(hass): + """Test unsuccessful discover, user adding the host via the form and then successful auth.""" + + await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( + "homeassistant.components.roon.config_flow.RoonApi", + return_value=RoonApiMock(), + ), patch( + "homeassistant.components.roon.config_flow.RoonDiscovery", + return_value=RoonDiscoveryFailedMock(), + ), patch( + "homeassistant.components.roon.async_setup", return_value=True + ), patch( + "homeassistant.components.roon.async_setup_entry", + return_value=True, + ): + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + # Should show the form if server was not discovered + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.1.1.1"} ) @@ -53,113 +136,85 @@ async def test_form_and_auth(hass): ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" assert result2["title"] == "Roon Labs Music Player" - assert result2["data"] == {"host": "1.1.1.1", "api_key": "good_token"} - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 + assert result2["data"] == { + "host": "1.1.1.1", + "api_key": "good_token", + "roon_server_id": "core_id", + } -async def test_form_no_token(hass): - """Test we handle no token being returned (timeout or not authorized).""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - with patch("homeassistant.components.roon.config_flow.TIMEOUT", 0,), patch( - "homeassistant.components.roon.const.AUTHENTICATE_TIMEOUT", +async def test_successful_discovery_no_auth(hass): + """Test successful discover, but failed auth.""" + + await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( + "homeassistant.components.roon.config_flow.RoonApi", + return_value=RoonApiMockNoToken(), + ), patch( + "homeassistant.components.roon.config_flow.RoonDiscovery", + return_value=RoonDiscoveryMock(), + ), patch( + "homeassistant.components.roon.config_flow.TIMEOUT", 0, ), patch( - "homeassistant.components.roon.config_flow.RoonApi", - return_value=RoonApiMock(None), + "homeassistant.components.roon.config_flow.AUTHENTICATE_TIMEOUT", + 0.01, + ), patch( + "homeassistant.components.roon.async_setup", return_value=True + ), patch( + "homeassistant.components.roon.async_setup_entry", + return_value=True, ): - await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": "1.1.1.1"} + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} ) + await hass.async_block_till_done() + + # Should go straight to link if server was discovered + assert result["type"] == "form" + assert result["step_id"] == "link" + assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) + await hass.async_block_till_done() - assert result2["type"] == "form" assert result2["errors"] == {"base": "invalid_auth"} -async def test_form_unknown_exception(hass): - """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - - with patch( - "homeassistant.components.roon.config_flow.RoonApi", - side_effect=Exception, - ): - await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": "1.1.1.1"} - ) - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={} - ) - - assert result2["type"] == "form" - assert result2["errors"] == {"base": "unknown"} - - -async def test_form_host_already_exists(hass): - """Test we add the host if the config exists and it isn't a duplicate.""" - - MockConfigEntry(domain=DOMAIN, data={CONF_HOST: "existing_host"}).add_to_hass(hass) +async def test_unexpected_exception(hass): + """Test successful discover, and unexpected exception during auth.""" await setup.async_setup_component(hass, "persistent_notification", {}) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == "form" - assert result["errors"] == {} - - with patch("homeassistant.components.roon.config_flow.TIMEOUT", 0,), patch( - "homeassistant.components.roon.const.AUTHENTICATE_TIMEOUT", - 0, - ), patch( + with patch( "homeassistant.components.roon.config_flow.RoonApi", - return_value=RoonApiMock("good_token"), + return_value=RoonApiMockException(), + ), patch( + "homeassistant.components.roon.config_flow.RoonDiscovery", + return_value=RoonDiscoveryMock(), ), patch( "homeassistant.components.roon.async_setup", return_value=True - ) as mock_setup, patch( + ), patch( "homeassistant.components.roon.async_setup_entry", return_value=True, - ) as mock_setup_entry: - await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": "1.1.1.1"} + ): + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} ) + await hass.async_block_till_done() + + # Should go straight to link if server was discovered + assert result["type"] == "form" + assert result["step_id"] == "link" + assert result["errors"] == {} + result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" - assert result2["title"] == "Roon Labs Music Player" - assert result2["data"] == {"host": "1.1.1.1", "api_key": "good_token"} - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 2 - - -async def test_form_duplicate_host(hass): - """Test we don't add the host if it's a duplicate.""" - - MockConfigEntry(domain=DOMAIN, data={CONF_HOST: "existing_host"}).add_to_hass(hass) - - await setup.async_setup_component(hass, "persistent_notification", {}) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) - assert result["type"] == "form" - assert result["errors"] == {} - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"host": "existing_host"} - ) - - assert result2["type"] == "form" - assert result2["errors"] == {"base": "duplicate_entry"} + assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/rpi_power/test_binary_sensor.py b/tests/components/rpi_power/test_binary_sensor.py index 873f654aa3b..13d452a2902 100644 --- a/tests/components/rpi_power/test_binary_sensor.py +++ b/tests/components/rpi_power/test_binary_sensor.py @@ -1,6 +1,7 @@ """Tests for rpi_power binary sensor.""" from datetime import timedelta import logging +from unittest.mock import MagicMock from homeassistant.components.rpi_power.binary_sensor import ( DESCRIPTION_NORMALIZED, @@ -11,7 +12,6 @@ from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from tests.async_mock import MagicMock from tests.common import MockConfigEntry, async_fire_time_changed, patch ENTITY_ID = "binary_sensor.rpi_power_status" diff --git a/tests/components/rpi_power/test_config_flow.py b/tests/components/rpi_power/test_config_flow.py index 090b6a6a793..7e302b51512 100644 --- a/tests/components/rpi_power/test_config_flow.py +++ b/tests/components/rpi_power/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for rpi_power config flow.""" +from unittest.mock import MagicMock + from homeassistant.components.rpi_power.const import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant @@ -8,7 +10,6 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) -from tests.async_mock import MagicMock from tests.common import patch MODULE = "homeassistant.components.rpi_power.config_flow.new_under_voltage" diff --git a/tests/components/ruckus_unleashed/__init__.py b/tests/components/ruckus_unleashed/__init__.py index 7efbfc03457..eff80a0387a 100644 --- a/tests/components/ruckus_unleashed/__init__.py +++ b/tests/components/ruckus_unleashed/__init__.py @@ -1,4 +1,6 @@ """Tests for the Ruckus Unleashed integration.""" +from unittest.mock import patch + from homeassistant.components.ruckus_unleashed import DOMAIN from homeassistant.components.ruckus_unleashed.const import ( API_ACCESS_POINT, @@ -15,7 +17,6 @@ from homeassistant.components.ruckus_unleashed.const import ( ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry DEFAULT_TITLE = "Ruckus Mesh" diff --git a/tests/components/ruckus_unleashed/test_config_flow.py b/tests/components/ruckus_unleashed/test_config_flow.py index 39112dd44aa..a11943bff00 100644 --- a/tests/components/ruckus_unleashed/test_config_flow.py +++ b/tests/components/ruckus_unleashed/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Ruckus Unleashed config flow.""" from datetime import timedelta +from unittest.mock import patch from pyruckus.exceptions import AuthenticationError @@ -7,7 +8,6 @@ from homeassistant import config_entries from homeassistant.components.ruckus_unleashed.const import DOMAIN from homeassistant.util import utcnow -from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.ruckus_unleashed import CONFIG, DEFAULT_SYSTEM_INFO, DEFAULT_TITLE diff --git a/tests/components/ruckus_unleashed/test_device_tracker.py b/tests/components/ruckus_unleashed/test_device_tracker.py index 38736b0117f..37bae441abf 100644 --- a/tests/components/ruckus_unleashed/test_device_tracker.py +++ b/tests/components/ruckus_unleashed/test_device_tracker.py @@ -1,5 +1,6 @@ """The sensor tests for the Ruckus Unleashed platform.""" from datetime import timedelta +from unittest.mock import patch from homeassistant.components.ruckus_unleashed import API_MAC, DOMAIN from homeassistant.components.ruckus_unleashed.const import API_AP, API_ID, API_NAME @@ -8,7 +9,6 @@ from homeassistant.helpers import entity_registry from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.util import utcnow -from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.ruckus_unleashed import ( DEFAULT_AP_INFO, diff --git a/tests/components/ruckus_unleashed/test_init.py b/tests/components/ruckus_unleashed/test_init.py index 5c694a5ee24..7b379a51011 100644 --- a/tests/components/ruckus_unleashed/test_init.py +++ b/tests/components/ruckus_unleashed/test_init.py @@ -1,4 +1,6 @@ """Test the Ruckus Unleashed config flow.""" +from unittest.mock import patch + from pyruckus.exceptions import AuthenticationError from homeassistant.components.ruckus_unleashed import ( @@ -19,7 +21,6 @@ from homeassistant.config_entries import ( ) from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from tests.async_mock import patch from tests.components.ruckus_unleashed import ( DEFAULT_AP_INFO, DEFAULT_SYSTEM_INFO, diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index f8a362e9842..ea78ecacb3e 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for Samsung TV config flow.""" +from unittest.mock import DEFAULT as DEFAULT_MOCK, Mock, PropertyMock, call, patch + import pytest from samsungctl.exceptions import AccessDenied, UnhandledResponse from samsungtvws.exceptions import ConnectionFailure @@ -18,8 +20,6 @@ from homeassistant.components.ssdp import ( ) from homeassistant.const import CONF_HOST, CONF_ID, CONF_METHOD, CONF_NAME, CONF_TOKEN -from tests.async_mock import DEFAULT as DEFAULT_MOCK, Mock, PropertyMock, call, patch - MOCK_USER_DATA = {CONF_HOST: "fake_host", CONF_NAME: "fake_name"} MOCK_SSDP_DATA = { ATTR_SSDP_LOCATION: "https://fake_host:12345/test", diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index 5ef47cb3106..bb19f120cf6 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -1,4 +1,6 @@ """Tests for the Samsung TV Integration.""" +from unittest.mock import Mock, call, patch + import pytest from homeassistant.components.media_player.const import DOMAIN, SUPPORT_TURN_ON @@ -16,8 +18,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, call, patch - ENTITY_ID = f"{DOMAIN}.fake_name" MOCK_CONFIG = { SAMSUNGTV_DOMAIN: [ diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index cc9a0f37ec8..6415f02cdf5 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -2,6 +2,7 @@ import asyncio from datetime import timedelta import logging +from unittest.mock import DEFAULT as DEFAULT_MOCK, Mock, PropertyMock, call, patch import pytest from samsungctl import exceptions @@ -52,7 +53,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import DEFAULT as DEFAULT_MOCK, Mock, PropertyMock, call, patch from tests.common import MockConfigEntry, async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake" diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index 068b0b91a2c..95703949525 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -2,6 +2,7 @@ # pylint: disable=protected-access import asyncio import unittest +from unittest.mock import Mock, patch import pytest @@ -23,7 +24,6 @@ from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.loader import bind_hass from homeassistant.setup import async_setup_component, setup_component -from tests.async_mock import Mock, patch from tests.common import async_mock_service, get_test_home_assistant from tests.components.logbook.test_init import MockLazyEventPartialState diff --git a/tests/components/season/test_sensor.py b/tests/components/season/test_sensor.py index 943036e9c00..d1c6cfb0b9f 100644 --- a/tests/components/season/test_sensor.py +++ b/tests/components/season/test_sensor.py @@ -1,5 +1,6 @@ """The tests for the Season sensor platform.""" from datetime import datetime +from unittest.mock import patch import pytest @@ -14,8 +15,6 @@ from homeassistant.components.season.sensor import ( from homeassistant.const import STATE_UNKNOWN from homeassistant.setup import async_setup_component -from tests.async_mock import patch - HEMISPHERE_NORTHERN = { "homeassistant": {"latitude": "48.864716", "longitude": "2.349014"}, "sensor": {"platform": "season", "type": "astronomical"}, diff --git a/tests/components/sense/test_config_flow.py b/tests/components/sense/test_config_flow.py index 44bd9c7265c..41cfdc017dd 100644 --- a/tests/components/sense/test_config_flow.py +++ b/tests/components/sense/test_config_flow.py @@ -1,11 +1,11 @@ """Test the Sense config flow.""" +from unittest.mock import patch + from sense_energy import SenseAPITimeoutException, SenseAuthenticationException from homeassistant import config_entries, setup from homeassistant.components.sense.const import DOMAIN -from tests.async_mock import patch - async def test_form(hass): """Test we get the form.""" diff --git a/tests/components/sensor/test_significant_change.py b/tests/components/sensor/test_significant_change.py new file mode 100644 index 00000000000..12b74345011 --- /dev/null +++ b/tests/components/sensor/test_significant_change.py @@ -0,0 +1,59 @@ +"""Test the sensor significant change platform.""" +from homeassistant.components.sensor.significant_change import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + async_check_significant_change, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_UNIT_OF_MEASUREMENT, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) + + +async def test_significant_change_temperature(): + """Detect temperature significant changes.""" + celsius_attrs = { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS, + } + assert not async_check_significant_change( + None, "12", celsius_attrs, "12", celsius_attrs + ) + assert async_check_significant_change( + None, "12", celsius_attrs, "13", celsius_attrs + ) + assert not async_check_significant_change( + None, "12.1", celsius_attrs, "12.2", celsius_attrs + ) + + freedom_attrs = { + ATTR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, + ATTR_UNIT_OF_MEASUREMENT: TEMP_FAHRENHEIT, + } + assert async_check_significant_change( + None, "70", freedom_attrs, "71", freedom_attrs + ) + assert not async_check_significant_change( + None, "70", freedom_attrs, "70.5", freedom_attrs + ) + + +async def test_significant_change_battery(): + """Detect battery significant changes.""" + attrs = { + ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY, + } + assert not async_check_significant_change(None, "100", attrs, "100", attrs) + assert async_check_significant_change(None, "100", attrs, "99", attrs) + + +async def test_significant_change_humidity(): + """Detect humidity significant changes.""" + attrs = { + ATTR_DEVICE_CLASS: DEVICE_CLASS_HUMIDITY, + } + assert not async_check_significant_change(None, "100", attrs, "100", attrs) + assert async_check_significant_change(None, "100", attrs, "99", attrs) diff --git a/tests/components/sentry/test_config_flow.py b/tests/components/sentry/test_config_flow.py index 9ae572123b5..82a1a70ec8b 100644 --- a/tests/components/sentry/test_config_flow.py +++ b/tests/components/sentry/test_config_flow.py @@ -1,5 +1,6 @@ """Test the sentry config flow.""" import logging +from unittest.mock import patch from sentry_sdk.utils import BadDsn @@ -18,7 +19,6 @@ from homeassistant.config_entries import SOURCE_USER from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/sentry/test_init.py b/tests/components/sentry/test_init.py index 95bda9738b8..e920437b2f7 100644 --- a/tests/components/sentry/test_init.py +++ b/tests/components/sentry/test_init.py @@ -1,5 +1,6 @@ """Tests for Sentry integration.""" import logging +from unittest.mock import MagicMock, Mock, patch import pytest @@ -17,7 +18,6 @@ from homeassistant.components.sentry.const import ( from homeassistant.const import __version__ as current_version from homeassistant.core import HomeAssistant -from tests.async_mock import MagicMock, Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/seventeentrack/test_sensor.py b/tests/components/seventeentrack/test_sensor.py index 798620e2af7..6519b435c0a 100644 --- a/tests/components/seventeentrack/test_sensor.py +++ b/tests/components/seventeentrack/test_sensor.py @@ -1,6 +1,7 @@ """Tests for the seventeentrack sensor.""" import datetime from typing import Union +from unittest.mock import MagicMock, patch from py17track.package import Package import pytest @@ -13,7 +14,6 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.setup import async_setup_component from homeassistant.util import utcnow -from tests.async_mock import MagicMock, patch from tests.common import async_fire_time_changed VALID_CONFIG_MINIMAL = { diff --git a/tests/components/sharkiq/test_config_flow.py b/tests/components/sharkiq/test_config_flow.py index 3183f6fdee2..890efbf1679 100644 --- a/tests/components/sharkiq/test_config_flow.py +++ b/tests/components/sharkiq/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Shark IQ config flow.""" +from unittest.mock import patch + import aiohttp import pytest from sharkiqpy import AylaApi, SharkIqAuthError @@ -9,7 +11,6 @@ from homeassistant.core import HomeAssistant from .const import CONFIG, TEST_PASSWORD, TEST_USERNAME, UNIQUE_ID -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/sharkiq/test_vacuum.py b/tests/components/sharkiq/test_vacuum.py index c548b59f5ba..edce5d75b2c 100644 --- a/tests/components/sharkiq/test_vacuum.py +++ b/tests/components/sharkiq/test_vacuum.py @@ -2,6 +2,7 @@ from copy import deepcopy import enum from typing import Any, Iterable, List, Optional +from unittest.mock import patch import pytest from sharkiqpy import AylaApi, SharkIqAuthError, SharkIqNotAuthedError, SharkIqVacuum @@ -56,7 +57,6 @@ from .const import ( TEST_USERNAME, ) -from tests.async_mock import patch from tests.common import MockConfigEntry VAC_ENTITY_ID = f"vacuum.{SHARK_DEVICE_DICT['product_name'].lower()}" @@ -200,7 +200,7 @@ async def test_device_properties( ): """Test device properties.""" registry = await hass.helpers.device_registry.async_get_registry() - device = registry.async_get_device({(DOMAIN, "AC000Wxxxxxxxxx")}, []) + device = registry.async_get_device({(DOMAIN, "AC000Wxxxxxxxxx")}) assert getattr(device, device_property) == target_value diff --git a/tests/components/shell_command/test_init.py b/tests/components/shell_command/test_init.py index f5ad37cc617..928c186bc11 100644 --- a/tests/components/shell_command/test_init.py +++ b/tests/components/shell_command/test_init.py @@ -3,12 +3,11 @@ import os import tempfile from typing import Tuple +from unittest.mock import MagicMock, patch from homeassistant.components import shell_command from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, patch - def mock_process_creator(error: bool = False): """Mock a coroutine that creates a process when yielded.""" diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 3f97c0ef317..804d5a75952 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -1,7 +1,62 @@ """Test configuration for Shelly.""" +from unittest.mock import AsyncMock, Mock, patch + import pytest -from tests.async_mock import patch +from homeassistant.components.shelly import ShellyDeviceWrapper +from homeassistant.components.shelly.const import ( + COAP, + DATA_CONFIG_ENTRY, + DOMAIN, + EVENT_SHELLY_CLICK, +) +from homeassistant.core import callback as ha_callback +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry, async_mock_service, mock_device_registry + +MOCK_SETTINGS = { + "name": "Test name", + "mode": "relay", + "device": { + "mac": "test-mac", + "hostname": "test-host", + "type": "SHSW-25", + "num_outputs": 2, + }, + "coiot": {"update_period": 15}, + "fw": "20201124-092159/v1.9.0@57ac4ad8", + "relays": [{"btn_type": "momentary"}, {"btn_type": "toggle"}], + "rollers": [{"positioning": True}], +} + +MOCK_BLOCKS = [ + Mock( + sensor_ids={"inputEvent": "S", "inputEventCnt": 2}, + channel="0", + type="relay", + set_state=AsyncMock(side_effect=lambda turn: {"ison": turn == "on"}), + ), + Mock( + sensor_ids={"roller": "stop", "rollerPos": 0}, + channel="1", + type="roller", + set_state=AsyncMock( + side_effect=lambda go, roller_pos=0: { + "current_pos": roller_pos, + "state": go, + } + ), + ), +] + + +MOCK_SHELLY = { + "mac": "test-mac", + "auth": False, + "fw": "20201124-092854/v1.9.0@57ac4ad8", + "num_outputs": 2, +} @pytest.fixture(autouse=True) @@ -9,3 +64,49 @@ def mock_coap(): """Mock out coap.""" with patch("homeassistant.components.shelly.get_coap_context"): yield + + +@pytest.fixture +def device_reg(hass): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") + + +@pytest.fixture +def events(hass): + """Yield caught shelly_click events.""" + ha_events = [] + hass.bus.async_listen(EVENT_SHELLY_CLICK, ha_callback(ha_events.append)) + yield ha_events + + +@pytest.fixture +async def coap_wrapper(hass): + """Setups a coap wrapper with mocked device.""" + await async_setup_component(hass, "shelly", {}) + + config_entry = MockConfigEntry(domain=DOMAIN, data={}) + config_entry.add_to_hass(hass) + + device = Mock( + blocks=MOCK_BLOCKS, + settings=MOCK_SETTINGS, + shelly=MOCK_SHELLY, + update=AsyncMock(), + ) + + hass.data[DOMAIN] = {DATA_CONFIG_ENTRY: {}} + hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id] = {} + wrapper = hass.data[DOMAIN][DATA_CONFIG_ENTRY][config_entry.entry_id][ + COAP + ] = ShellyDeviceWrapper(hass, config_entry, device) + + await wrapper.async_setup() + + return wrapper diff --git a/tests/components/shelly/test_config_flow.py b/tests/components/shelly/test_config_flow.py index 2850be11450..60f899296f6 100644 --- a/tests/components/shelly/test_config_flow.py +++ b/tests/components/shelly/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Shelly config flow.""" import asyncio +from unittest.mock import AsyncMock, Mock, patch import aiohttp import aioshelly @@ -8,7 +9,6 @@ import pytest from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.shelly.const import DOMAIN -from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry MOCK_SETTINGS = { @@ -53,7 +53,7 @@ async def test_form(hass): ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result2["title"] == "Test name" assert result2["data"] == { "host": "1.1.1.1", @@ -68,7 +68,7 @@ async def test_title_without_name(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} settings = MOCK_SETTINGS.copy() @@ -97,7 +97,7 @@ async def test_title_without_name(hass): ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result2["title"] == "shelly1pm-12345" assert result2["data"] == { "host": "1.1.1.1", @@ -111,7 +111,7 @@ async def test_form_auth(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} with patch( @@ -123,7 +123,7 @@ async def test_form_auth(hass): {"host": "1.1.1.1"}, ) - assert result2["type"] == "form" + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} with patch( @@ -145,7 +145,7 @@ async def test_form_auth(hass): ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result3["title"] == "Test name" assert result3["data"] == { "host": "1.1.1.1", @@ -172,7 +172,7 @@ async def test_form_errors_get_info(hass, error): {"host": "1.1.1.1"}, ) - assert result2["type"] == "form" + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["errors"] == {"base": base_error} @@ -194,7 +194,7 @@ async def test_form_errors_test_connection(hass, error): {"host": "1.1.1.1"}, ) - assert result2["type"] == "form" + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["errors"] == {"base": base_error} @@ -219,7 +219,7 @@ async def test_form_already_configured(hass): {"host": "1.1.1.1"}, ) - assert result2["type"] == "abort" + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result2["reason"] == "already_configured" # Test config entry got updated with latest IP @@ -241,19 +241,38 @@ async def test_user_setup_ignored_device(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) + settings = MOCK_SETTINGS.copy() + settings["device"]["type"] = "SHSW-1" + settings["fw"] = "20201124-092534/v1.9.0@57ac4ad8" + with patch( "aioshelly.get_info", return_value={"mac": "test-mac", "type": "SHSW-1", "auth": False}, - ): + ), patch( + "aioshelly.Device.create", + new=AsyncMock( + return_value=Mock( + settings=settings, + ) + ), + ), patch( + "homeassistant.components.shelly.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.shelly.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {"host": "1.1.1.1"}, ) - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY # Test config entry got updated with latest IP assert entry.data["host"] == "1.1.1.1" + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 async def test_form_firmware_unsupported(hass): @@ -268,7 +287,7 @@ async def test_form_firmware_unsupported(hass): {"host": "1.1.1.1"}, ) - assert result2["type"] == "abort" + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result2["reason"] == "unsupported_firmware" @@ -302,7 +321,7 @@ async def test_form_auth_errors_test_connection(hass, error): result2["flow_id"], {"username": "test username", "password": "test password"}, ) - assert result3["type"] == "form" + assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM assert result3["errors"] == {"base": base_error} @@ -319,7 +338,7 @@ async def test_zeroconf(hass): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == "form" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} context = next( flow["context"] @@ -346,7 +365,7 @@ async def test_zeroconf(hass): ) await hass.async_block_till_done() - assert result2["type"] == "create_entry" + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result2["title"] == "Test name" assert result2["data"] == { "host": "1.1.1.1", @@ -372,7 +391,7 @@ async def test_zeroconf_confirm_error(hass, error): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == "form" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} with patch( @@ -384,7 +403,7 @@ async def test_zeroconf_confirm_error(hass, error): {}, ) - assert result2["type"] == "form" + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["errors"] == {"base": base_error} @@ -405,7 +424,7 @@ async def test_zeroconf_already_configured(hass): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == "abort" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" # Test config entry got updated with latest IP @@ -421,7 +440,7 @@ async def test_zeroconf_firmware_unsupported(hass): context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == "abort" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "unsupported_firmware" @@ -433,7 +452,7 @@ async def test_zeroconf_cannot_connect(hass): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == "abort" + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "cannot_connect" @@ -450,14 +469,14 @@ async def test_zeroconf_require_auth(hass): data=DISCOVERY_INFO, context={"source": config_entries.SOURCE_ZEROCONF}, ) - assert result["type"] == "form" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} result2 = await hass.config_entries.flow.async_configure( result["flow_id"], {}, ) - assert result2["type"] == "form" + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["errors"] == {} with patch( @@ -479,7 +498,7 @@ async def test_zeroconf_require_auth(hass): ) await hass.async_block_till_done() - assert result3["type"] == "create_entry" + assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result3["title"] == "Test name" assert result3["data"] == { "host": "1.1.1.1", diff --git a/tests/components/shelly/test_cover.py b/tests/components/shelly/test_cover.py new file mode 100644 index 00000000000..5c34fcc1bf5 --- /dev/null +++ b/tests/components/shelly/test_cover.py @@ -0,0 +1,93 @@ +"""The scene tests for the myq platform.""" +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_POSITION, + DOMAIN as COVER_DOMAIN, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + SERVICE_SET_COVER_POSITION, + SERVICE_STOP_COVER, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, +) +from homeassistant.const import ATTR_ENTITY_ID + +ROLLER_BLOCK_ID = 1 + + +async def test_services(hass, coap_wrapper, monkeypatch): + """Test device turn on/off services.""" + assert coap_wrapper + + monkeypatch.setitem(coap_wrapper.device.settings, "mode", "roller") + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, COVER_DOMAIN) + ) + await hass.async_block_till_done() + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: "cover.test_name", ATTR_POSITION: 50}, + blocking=True, + ) + state = hass.states.get("cover.test_name") + assert state.attributes[ATTR_CURRENT_POSITION] == 50 + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_OPEN_COVER, + {ATTR_ENTITY_ID: "cover.test_name"}, + blocking=True, + ) + assert hass.states.get("cover.test_name").state == STATE_OPENING + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: "cover.test_name"}, + blocking=True, + ) + assert hass.states.get("cover.test_name").state == STATE_CLOSING + + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_STOP_COVER, + {ATTR_ENTITY_ID: "cover.test_name"}, + blocking=True, + ) + assert hass.states.get("cover.test_name").state == STATE_CLOSED + + +async def test_update(hass, coap_wrapper, monkeypatch): + """Test device update.""" + assert coap_wrapper + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, COVER_DOMAIN) + ) + await hass.async_block_till_done() + + monkeypatch.setattr(coap_wrapper.device.blocks[ROLLER_BLOCK_ID], "rollerPos", 0) + await hass.helpers.entity_component.async_update_entity("cover.test_name") + await hass.async_block_till_done() + assert hass.states.get("cover.test_name").state == STATE_CLOSED + + monkeypatch.setattr(coap_wrapper.device.blocks[ROLLER_BLOCK_ID], "rollerPos", 100) + await hass.helpers.entity_component.async_update_entity("cover.test_name") + await hass.async_block_till_done() + assert hass.states.get("cover.test_name").state == STATE_OPEN + + +async def test_no_roller_blocks(hass, coap_wrapper, monkeypatch): + """Test device without roller blocks.""" + assert coap_wrapper + + monkeypatch.setattr(coap_wrapper.device.blocks[ROLLER_BLOCK_ID], "type", None) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, COVER_DOMAIN) + ) + await hass.async_block_till_done() + assert hass.states.get("cover.test_name") is None diff --git a/tests/components/shelly/test_device_trigger.py b/tests/components/shelly/test_device_trigger.py new file mode 100644 index 00000000000..a725f5a1f30 --- /dev/null +++ b/tests/components/shelly/test_device_trigger.py @@ -0,0 +1,173 @@ +"""The tests for Shelly device triggers.""" +import pytest + +from homeassistant import setup +from homeassistant.components import automation +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) +from homeassistant.components.shelly.const import ( + ATTR_CHANNEL, + ATTR_CLICK_TYPE, + CONF_SUBTYPE, + DOMAIN, + EVENT_SHELLY_CLICK, +) +from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_PLATFORM, CONF_TYPE +from homeassistant.helpers import device_registry +from homeassistant.setup import async_setup_component + +from tests.common import ( + MockConfigEntry, + assert_lists_same, + async_get_device_automations, + async_mock_service, +) + + +async def test_get_triggers(hass, coap_wrapper): + """Test we get the expected triggers from a shelly.""" + assert coap_wrapper + expected_triggers = [ + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: coap_wrapper.device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: "single", + CONF_SUBTYPE: "button1", + }, + { + CONF_PLATFORM: "device", + CONF_DEVICE_ID: coap_wrapper.device_id, + CONF_DOMAIN: DOMAIN, + CONF_TYPE: "long", + CONF_SUBTYPE: "button1", + }, + ] + + triggers = await async_get_device_automations( + hass, "trigger", coap_wrapper.device_id + ) + + assert_lists_same(triggers, expected_triggers) + + +async def test_get_triggers_for_invalid_device_id(hass, device_reg, coap_wrapper): + """Test error raised for invalid shelly device_id.""" + assert coap_wrapper + config_entry = MockConfigEntry(domain=DOMAIN, data={}) + config_entry.add_to_hass(hass) + invalid_device = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + ) + + with pytest.raises(InvalidDeviceAutomationConfig): + await async_get_device_automations(hass, "trigger", invalid_device.id) + + +async def test_if_fires_on_click_event(hass, calls, coap_wrapper): + """Test for click_event trigger firing.""" + assert coap_wrapper + await setup.async_setup_component(hass, "persistent_notification", {}) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: coap_wrapper.device_id, + CONF_TYPE: "single", + CONF_SUBTYPE: "button1", + }, + "action": { + "service": "test.automation", + "data_template": {"some": "test_trigger_single_click"}, + }, + }, + ] + }, + ) + + message = { + CONF_DEVICE_ID: coap_wrapper.device_id, + ATTR_CLICK_TYPE: "single", + ATTR_CHANNEL: 1, + } + hass.bus.async_fire(EVENT_SHELLY_CLICK, message) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data["some"] == "test_trigger_single_click" + + +async def test_validate_trigger_config_no_device(hass, calls, coap_wrapper): + """Test for click_event with no device.""" + assert coap_wrapper + await setup.async_setup_component(hass, "persistent_notification", {}) + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: "no_device", + CONF_TYPE: "single", + CONF_SUBTYPE: "button1", + }, + "action": { + "service": "test.automation", + "data_template": {"some": "test_trigger_single_click"}, + }, + }, + ] + }, + ) + message = {CONF_DEVICE_ID: "no_device", ATTR_CLICK_TYPE: "single", ATTR_CHANNEL: 1} + hass.bus.async_fire(EVENT_SHELLY_CLICK, message) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].data["some"] == "test_trigger_single_click" + + +async def test_validate_trigger_invalid_triggers(hass, coap_wrapper): + """Test for click_event with invalid triggers.""" + assert coap_wrapper + notification_calls = async_mock_service(hass, "persistent_notification", "create") + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + CONF_PLATFORM: "device", + CONF_DOMAIN: DOMAIN, + CONF_DEVICE_ID: coap_wrapper.device_id, + CONF_TYPE: "single", + CONF_SUBTYPE: "button3", + }, + "action": { + "service": "test.automation", + "data_template": {"some": "test_trigger_single_click"}, + }, + }, + ] + }, + ) + + assert len(notification_calls) == 1 + assert ( + "The following integrations and platforms could not be set up" + in notification_calls[0].data["message"] + ) diff --git a/tests/components/shelly/test_logbook.py b/tests/components/shelly/test_logbook.py new file mode 100644 index 00000000000..9cfda9ddcaa --- /dev/null +++ b/tests/components/shelly/test_logbook.py @@ -0,0 +1,62 @@ +"""The tests for Shelly logbook.""" +from homeassistant.components import logbook +from homeassistant.components.shelly.const import ( + ATTR_CHANNEL, + ATTR_CLICK_TYPE, + ATTR_DEVICE, + DOMAIN, + EVENT_SHELLY_CLICK, +) +from homeassistant.const import ATTR_DEVICE_ID +from homeassistant.setup import async_setup_component + +from tests.components.logbook.test_init import MockLazyEventPartialState + + +async def test_humanify_shelly_click_event(hass, coap_wrapper): + """Test humanifying Shelly click event.""" + assert coap_wrapper + hass.config.components.add("recorder") + assert await async_setup_component(hass, "logbook", {}) + entity_attr_cache = logbook.EntityAttributeCache(hass) + + event1, event2 = list( + logbook.humanify( + hass, + [ + MockLazyEventPartialState( + EVENT_SHELLY_CLICK, + { + ATTR_DEVICE_ID: coap_wrapper.device_id, + ATTR_DEVICE: "shellyix3-12345678", + ATTR_CLICK_TYPE: "single", + ATTR_CHANNEL: 1, + }, + ), + MockLazyEventPartialState( + EVENT_SHELLY_CLICK, + { + ATTR_DEVICE_ID: "no_device_id", + ATTR_DEVICE: "shellyswitch25-12345678", + ATTR_CLICK_TYPE: "long", + ATTR_CHANNEL: 2, + }, + ), + ], + entity_attr_cache, + {}, + ) + ) + + assert event1["name"] == "Shelly" + assert event1["domain"] == DOMAIN + assert ( + event1["message"] == "'single' click event for Test name channel 1 was fired." + ) + + assert event2["name"] == "Shelly" + assert event2["domain"] == DOMAIN + assert ( + event2["message"] + == "'long' click event for shellyswitch25-12345678 channel 2 was fired." + ) diff --git a/tests/components/shelly/test_switch.py b/tests/components/shelly/test_switch.py new file mode 100644 index 00000000000..b1dcc05bb80 --- /dev/null +++ b/tests/components/shelly/test_switch.py @@ -0,0 +1,85 @@ +"""The scene tests for the myq platform.""" +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) + +RELAY_BLOCK_ID = 0 + + +async def test_services(hass, coap_wrapper): + """Test device turn on/off services.""" + assert coap_wrapper + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, SWITCH_DOMAIN) + ) + await hass.async_block_till_done() + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.test_name_channel_1"}, + blocking=True, + ) + assert hass.states.get("switch.test_name_channel_1").state == STATE_ON + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.test_name_channel_1"}, + blocking=True, + ) + assert hass.states.get("switch.test_name_channel_1").state == STATE_OFF + + +async def test_update(hass, coap_wrapper, monkeypatch): + """Test device update.""" + assert coap_wrapper + + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, SWITCH_DOMAIN) + ) + await hass.async_block_till_done() + + monkeypatch.setattr(coap_wrapper.device.blocks[RELAY_BLOCK_ID], "output", False) + await hass.helpers.entity_component.async_update_entity( + "switch.test_name_channel_1" + ) + await hass.async_block_till_done() + assert hass.states.get("switch.test_name_channel_1").state == STATE_OFF + + monkeypatch.setattr(coap_wrapper.device.blocks[RELAY_BLOCK_ID], "output", True) + await hass.helpers.entity_component.async_update_entity( + "switch.test_name_channel_1" + ) + await hass.async_block_till_done() + assert hass.states.get("switch.test_name_channel_1").state == STATE_ON + + +async def test_no_relay_blocks(hass, coap_wrapper, monkeypatch): + """Test device without relay blocks.""" + assert coap_wrapper + + monkeypatch.setattr(coap_wrapper.device.blocks[RELAY_BLOCK_ID], "type", "roller") + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, SWITCH_DOMAIN) + ) + await hass.async_block_till_done() + assert hass.states.get("switch.test_name_channel_1") is None + + +async def test_device_mode_roller(hass, coap_wrapper, monkeypatch): + """Test switch device in roller mode.""" + assert coap_wrapper + + monkeypatch.setitem(coap_wrapper.device.settings, "mode", "roller") + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(coap_wrapper.entry, SWITCH_DOMAIN) + ) + await hass.async_block_till_done() + assert hass.states.get("switch.test_name_channel_1") is None diff --git a/tests/components/shopping_list/conftest.py b/tests/components/shopping_list/conftest.py index 8307a6845b1..596a8c87cd3 100644 --- a/tests/components/shopping_list/conftest.py +++ b/tests/components/shopping_list/conftest.py @@ -1,9 +1,10 @@ """Shopping list test helpers.""" +from unittest.mock import patch + import pytest from homeassistant.components.shopping_list import intent as sl_intent -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/shopping_list/test_init.py b/tests/components/shopping_list/test_init.py index b8930114665..0be4c70ef18 100644 --- a/tests/components/shopping_list/test_init.py +++ b/tests/components/shopping_list/test_init.py @@ -1,6 +1,10 @@ """Test shopping list component.""" -from homeassistant.components.websocket_api.const import TYPE_RESULT +from homeassistant.components.websocket_api.const import ( + ERR_INVALID_FORMAT, + ERR_NOT_FOUND, + TYPE_RESULT, +) from homeassistant.const import HTTP_NOT_FOUND from homeassistant.helpers import intent @@ -311,3 +315,125 @@ async def test_ws_add_item_fail(hass, hass_ws_client, sl_setup): msg = await client.receive_json() assert msg["success"] is False assert len(hass.data["shopping_list"].items) == 0 + + +async def test_ws_reorder_items(hass, hass_ws_client, sl_setup): + """Test reordering shopping_list items websocket command.""" + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} + ) + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "wine"}} + ) + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "apple"}} + ) + + beer_id = hass.data["shopping_list"].items[0]["id"] + wine_id = hass.data["shopping_list"].items[1]["id"] + apple_id = hass.data["shopping_list"].items[2]["id"] + + client = await hass_ws_client(hass) + await client.send_json( + { + "id": 6, + "type": "shopping_list/items/reorder", + "item_ids": [wine_id, apple_id, beer_id], + } + ) + msg = await client.receive_json() + assert msg["success"] is True + assert hass.data["shopping_list"].items[0] == { + "id": wine_id, + "name": "wine", + "complete": False, + } + assert hass.data["shopping_list"].items[1] == { + "id": apple_id, + "name": "apple", + "complete": False, + } + assert hass.data["shopping_list"].items[2] == { + "id": beer_id, + "name": "beer", + "complete": False, + } + + # Mark wine as completed. + await client.send_json( + { + "id": 7, + "type": "shopping_list/items/update", + "item_id": wine_id, + "complete": True, + } + ) + _ = await client.receive_json() + + await client.send_json( + { + "id": 8, + "type": "shopping_list/items/reorder", + "item_ids": [apple_id, beer_id], + } + ) + msg = await client.receive_json() + assert msg["success"] is True + assert hass.data["shopping_list"].items[0] == { + "id": apple_id, + "name": "apple", + "complete": False, + } + assert hass.data["shopping_list"].items[1] == { + "id": beer_id, + "name": "beer", + "complete": False, + } + assert hass.data["shopping_list"].items[2] == { + "id": wine_id, + "name": "wine", + "complete": True, + } + + +async def test_ws_reorder_items_failure(hass, hass_ws_client, sl_setup): + """Test reordering shopping_list items websocket command.""" + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "beer"}} + ) + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "wine"}} + ) + await intent.async_handle( + hass, "test", "HassShoppingListAddItem", {"item": {"value": "apple"}} + ) + + beer_id = hass.data["shopping_list"].items[0]["id"] + wine_id = hass.data["shopping_list"].items[1]["id"] + apple_id = hass.data["shopping_list"].items[2]["id"] + + client = await hass_ws_client(hass) + + # Testing sending bad item id. + await client.send_json( + { + "id": 8, + "type": "shopping_list/items/reorder", + "item_ids": [wine_id, apple_id, beer_id, "BAD_ID"], + } + ) + msg = await client.receive_json() + assert msg["success"] is False + assert msg["error"]["code"] == ERR_NOT_FOUND + + # Testing not sending all unchecked item ids. + await client.send_json( + { + "id": 9, + "type": "shopping_list/items/reorder", + "item_ids": [wine_id, apple_id], + } + ) + msg = await client.receive_json() + assert msg["success"] is False + assert msg["error"]["code"] == ERR_INVALID_FORMAT diff --git a/tests/components/signal_messenger/test_notify.py b/tests/components/signal_messenger/test_notify.py index 407068e6f3e..4bb6cbdd197 100644 --- a/tests/components/signal_messenger/test_notify.py +++ b/tests/components/signal_messenger/test_notify.py @@ -3,6 +3,7 @@ import os import tempfile import unittest +from unittest.mock import patch from pysignalclirestapi import SignalCliRestApi import requests_mock @@ -10,8 +11,6 @@ import requests_mock import homeassistant.components.signal_messenger.notify as signalmessenger from homeassistant.setup import async_setup_component -from tests.async_mock import patch - BASE_COMPONENT = "notify" diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index ec7ad592f15..8f9d3a9897c 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -1,4 +1,6 @@ """Define tests for the SimpliSafe config flow.""" +from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch + from simplipy.errors import ( InvalidCredentialsError, PendingAuthorizationError, @@ -10,7 +12,6 @@ from homeassistant.components.simplisafe import DOMAIN from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME -from tests.async_mock import AsyncMock, MagicMock, PropertyMock, patch from tests.common import MockConfigEntry diff --git a/tests/components/slack/test_notify.py b/tests/components/slack/test_notify.py index bfd78b20900..9fc6784a09e 100644 --- a/tests/components/slack/test_notify.py +++ b/tests/components/slack/test_notify.py @@ -1,9 +1,111 @@ """Test slack notifications.""" -from unittest.mock import Mock +import copy +import logging +from typing import List +from unittest.mock import AsyncMock, Mock, patch -from homeassistant.components.slack.notify import SlackNotificationService +from _pytest.logging import LogCaptureFixture +import aiohttp +from slack.errors import SlackApiError -from tests.async_mock import AsyncMock +from homeassistant.components import notify +from homeassistant.components.slack import DOMAIN +from homeassistant.components.slack.notify import ( + CONF_DEFAULT_CHANNEL, + SlackNotificationService, +) +from homeassistant.const import ( + CONF_API_KEY, + CONF_ICON, + CONF_NAME, + CONF_PLATFORM, + CONF_USERNAME, +) +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.setup import async_setup_component + +MODULE_PATH = "homeassistant.components.slack.notify" +SERVICE_NAME = f"notify_{DOMAIN}" + +DEFAULT_CONFIG = { + notify.DOMAIN: [ + { + CONF_PLATFORM: DOMAIN, + CONF_NAME: SERVICE_NAME, + CONF_API_KEY: "12345", + CONF_DEFAULT_CHANNEL: "channel", + } + ] +} + + +def filter_log_records(caplog: LogCaptureFixture) -> List[logging.LogRecord]: + """Filter all unrelated log records.""" + return [ + rec for rec in caplog.records if rec.name.endswith(f"{DOMAIN}.{notify.DOMAIN}") + ] + + +async def test_setup(hass: HomeAssistantType, caplog: LogCaptureFixture): + """Test setup slack notify.""" + config = DEFAULT_CONFIG + + with patch( + MODULE_PATH + ".aiohttp_client", + **{"async_get_clientsession.return_value": (session := Mock())}, + ), patch( + MODULE_PATH + ".WebClient", + return_value=(client := AsyncMock()), + ) as mock_client: + + await async_setup_component(hass, notify.DOMAIN, config) + await hass.async_block_till_done() + assert hass.services.has_service(notify.DOMAIN, SERVICE_NAME) + caplog_records_slack = filter_log_records(caplog) + assert len(caplog_records_slack) == 0 + mock_client.assert_called_with(token="12345", run_async=True, session=session) + client.auth_test.assert_called_once_with() + + +async def test_setup_clientError(hass: HomeAssistantType, caplog: LogCaptureFixture): + """Test setup slack notify with aiohttp.ClientError exception.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config[notify.DOMAIN][0].update({CONF_USERNAME: "user", CONF_ICON: "icon"}) + + with patch( + MODULE_PATH + ".aiohttp_client", + **{"async_get_clientsession.return_value": Mock()}, + ), patch(MODULE_PATH + ".WebClient", return_value=(client := AsyncMock())): + + client.auth_test.side_effect = [aiohttp.ClientError] + await async_setup_component(hass, notify.DOMAIN, config) + await hass.async_block_till_done() + assert hass.services.has_service(notify.DOMAIN, SERVICE_NAME) + caplog_records_slack = filter_log_records(caplog) + assert len(caplog_records_slack) == 1 + record = caplog_records_slack[0] + assert record.levelno == logging.WARNING + assert aiohttp.ClientError.__qualname__ in record.message + + +async def test_setup_slackApiError(hass: HomeAssistantType, caplog: LogCaptureFixture): + """Test setup slack notify with SlackApiError exception.""" + config = DEFAULT_CONFIG + + with patch( + MODULE_PATH + ".aiohttp_client", + **{"async_get_clientsession.return_value": Mock()}, + ), patch(MODULE_PATH + ".WebClient", return_value=(client := AsyncMock())): + + client.auth_test.side_effect = [err := SlackApiError("msg", "resp")] + await async_setup_component(hass, notify.DOMAIN, config) + await hass.async_block_till_done() + assert hass.services.has_service(notify.DOMAIN, SERVICE_NAME) is False + caplog_records_slack = filter_log_records(caplog) + assert len(caplog_records_slack) == 1 + record = caplog_records_slack[0] + assert record.levelno == logging.ERROR + assert err.__class__.__qualname__ in record.message async def test_message_includes_default_emoji(): diff --git a/tests/components/sleepiq/test_binary_sensor.py b/tests/components/sleepiq/test_binary_sensor.py index c1577a21cd2..c158554b278 100644 --- a/tests/components/sleepiq/test_binary_sensor.py +++ b/tests/components/sleepiq/test_binary_sensor.py @@ -1,8 +1,9 @@ """The tests for SleepIQ binary sensor platform.""" +from unittest.mock import MagicMock + from homeassistant.components.sleepiq import binary_sensor as sleepiq from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock from tests.components.sleepiq.test_init import mock_responses CONFIG = {"username": "foo", "password": "bar"} diff --git a/tests/components/sleepiq/test_init.py b/tests/components/sleepiq/test_init.py index 70aaca2e17d..a5e8e43ae07 100644 --- a/tests/components/sleepiq/test_init.py +++ b/tests/components/sleepiq/test_init.py @@ -1,8 +1,9 @@ """The tests for the SleepIQ component.""" +from unittest.mock import MagicMock, patch + from homeassistant import setup import homeassistant.components.sleepiq as sleepiq -from tests.async_mock import MagicMock, patch from tests.common import load_fixture CONFIG = {"sleepiq": {"username": "foo", "password": "bar"}} diff --git a/tests/components/sleepiq/test_sensor.py b/tests/components/sleepiq/test_sensor.py index 53ee4de973f..559e808f554 100644 --- a/tests/components/sleepiq/test_sensor.py +++ b/tests/components/sleepiq/test_sensor.py @@ -1,8 +1,9 @@ """The tests for SleepIQ sensor platform.""" +from unittest.mock import MagicMock + import homeassistant.components.sleepiq.sensor as sleepiq from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock from tests.components.sleepiq.test_init import mock_responses CONFIG = {"username": "foo", "password": "bar"} diff --git a/tests/components/smappee/test_config_flow.py b/tests/components/smappee/test_config_flow.py index 55d063c2b1c..cba962d3e44 100644 --- a/tests/components/smappee/test_config_flow.py +++ b/tests/components/smappee/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Smappee component config flow module.""" +from unittest.mock import patch + from homeassistant import data_entry_flow, setup from homeassistant.components.smappee.const import ( CONF_HOSTNAME, @@ -12,7 +14,6 @@ from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow -from tests.async_mock import patch from tests.common import MockConfigEntry CLIENT_ID = "1234" diff --git a/tests/components/smappee/test_init.py b/tests/components/smappee/test_init.py index 9a81441e8b3..a3cd9897d9c 100644 --- a/tests/components/smappee/test_init.py +++ b/tests/components/smappee/test_init.py @@ -1,8 +1,9 @@ """Tests for the Smappee component init module.""" +from unittest.mock import patch + from homeassistant.components.smappee.const import DOMAIN from homeassistant.config_entries import SOURCE_ZEROCONF -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/smart_meter_texas/test_config_flow.py b/tests/components/smart_meter_texas/test_config_flow.py index 4908a50e57d..d0108f2ee09 100644 --- a/tests/components/smart_meter_texas/test_config_flow.py +++ b/tests/components/smart_meter_texas/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Smart Meter Texas config flow.""" import asyncio +from unittest.mock import patch from aiohttp import ClientError import pytest @@ -12,7 +13,6 @@ from homeassistant import config_entries, setup from homeassistant.components.smart_meter_texas.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry TEST_LOGIN = {CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"} diff --git a/tests/components/smart_meter_texas/test_init.py b/tests/components/smart_meter_texas/test_init.py index 861425601ec..7db4113e3cf 100644 --- a/tests/components/smart_meter_texas/test_init.py +++ b/tests/components/smart_meter_texas/test_init.py @@ -1,4 +1,6 @@ """Test the Smart Meter Texas module.""" +from unittest.mock import patch + from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, SERVICE_UPDATE_ENTITY, @@ -15,8 +17,6 @@ from homeassistant.setup import async_setup_component from .conftest import TEST_ENTITY_ID, setup_integration -from tests.async_mock import patch - async def test_setup_with_no_config(hass): """Test that no config is successful.""" diff --git a/tests/components/smart_meter_texas/test_sensor.py b/tests/components/smart_meter_texas/test_sensor.py index 104da011d90..774a369c83a 100644 --- a/tests/components/smart_meter_texas/test_sensor.py +++ b/tests/components/smart_meter_texas/test_sensor.py @@ -1,4 +1,6 @@ """Test the Smart Meter Texas sensor entity.""" +from unittest.mock import patch + from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, SERVICE_UPDATE_ENTITY, @@ -13,8 +15,6 @@ from homeassistant.setup import async_setup_component from .conftest import TEST_ENTITY_ID, refresh_data, setup_integration -from tests.async_mock import patch - async def test_sensor(hass, config_entry, aioclient_mock): """Test that the sensor is setup.""" diff --git a/tests/components/smarthab/test_config_flow.py b/tests/components/smarthab/test_config_flow.py index 6b8c58b1f70..6201d6f6f28 100644 --- a/tests/components/smarthab/test_config_flow.py +++ b/tests/components/smarthab/test_config_flow.py @@ -1,12 +1,12 @@ """Test the SmartHab config flow.""" +from unittest.mock import patch + import pysmarthab from homeassistant import config_entries, setup from homeassistant.components.smarthab import DOMAIN from homeassistant.const import CONF_EMAIL, CONF_PASSWORD -from tests.async_mock import patch - async def test_form(hass): """Test we get the form.""" diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index 8588c81654e..b99309bea52 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -1,5 +1,6 @@ """Test configuration and mocks for the SmartThings component.""" import secrets +from unittest.mock import Mock, patch from uuid import uuid4 from pysmartthings import ( @@ -45,7 +46,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry from tests.components.light.conftest import mock_light_profiles # noqa diff --git a/tests/components/smartthings/test_binary_sensor.py b/tests/components/smartthings/test_binary_sensor.py index b007fff7caf..6931b3dfbb5 100644 --- a/tests/components/smartthings/test_binary_sensor.py +++ b/tests/components/smartthings/test_binary_sensor.py @@ -57,7 +57,7 @@ async def test_entity_and_device_attributes(hass, device_factory): entry = entity_registry.async_get("binary_sensor.motion_sensor_1_motion") assert entry assert entry.unique_id == f"{device.device_id}.{Attribute.motion}" - entry = device_registry.async_get_device({(DOMAIN, device.device_id)}, []) + entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) assert entry assert entry.name == device.label assert entry.model == device.device_type_name diff --git a/tests/components/smartthings/test_climate.py b/tests/components/smartthings/test_climate.py index 4229bd7cf94..11f7695a775 100644 --- a/tests/components/smartthings/test_climate.py +++ b/tests/components/smartthings/test_climate.py @@ -576,7 +576,7 @@ async def test_entity_and_device_attributes(hass, thermostat): assert entry assert entry.unique_id == thermostat.device_id - entry = device_registry.async_get_device({(DOMAIN, thermostat.device_id)}, []) + entry = device_registry.async_get_device({(DOMAIN, thermostat.device_id)}) assert entry assert entry.name == thermostat.label assert entry.model == thermostat.device_type_name diff --git a/tests/components/smartthings/test_config_flow.py b/tests/components/smartthings/test_config_flow.py index b776959ea5b..d8e0f6ed784 100644 --- a/tests/components/smartthings/test_config_flow.py +++ b/tests/components/smartthings/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for the SmartThings config flow module.""" +from unittest.mock import AsyncMock, Mock, patch from uuid import uuid4 from aiohttp import ClientResponseError @@ -23,7 +24,6 @@ from homeassistant.const import ( HTTP_UNAUTHORIZED, ) -from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py index 9c5a80e27fb..0483480cb8a 100644 --- a/tests/components/smartthings/test_cover.py +++ b/tests/components/smartthings/test_cover.py @@ -40,7 +40,7 @@ async def test_entity_and_device_attributes(hass, device_factory): assert entry assert entry.unique_id == device.device_id - entry = device_registry.async_get_device({(DOMAIN, device.device_id)}, []) + entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) assert entry assert entry.name == device.label assert entry.model == device.device_type_name diff --git a/tests/components/smartthings/test_fan.py b/tests/components/smartthings/test_fan.py index 6b8eb56d65c..0ebef7e7323 100644 --- a/tests/components/smartthings/test_fan.py +++ b/tests/components/smartthings/test_fan.py @@ -62,7 +62,7 @@ async def test_entity_and_device_attributes(hass, device_factory): assert entry assert entry.unique_id == device.device_id - entry = device_registry.async_get_device({(DOMAIN, device.device_id)}, []) + entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) assert entry assert entry.name == device.label assert entry.model == device.device_type_name diff --git a/tests/components/smartthings/test_init.py b/tests/components/smartthings/test_init.py index 931e895cb65..9024b72bb85 100644 --- a/tests/components/smartthings/test_init.py +++ b/tests/components/smartthings/test_init.py @@ -1,4 +1,5 @@ """Tests for the SmartThings component init module.""" +from unittest.mock import Mock, patch from uuid import uuid4 from aiohttp import ClientConnectionError, ClientResponseError @@ -22,7 +23,6 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/smartthings/test_light.py b/tests/components/smartthings/test_light.py index 43a73113fec..bd9557c6b97 100644 --- a/tests/components/smartthings/test_light.py +++ b/tests/components/smartthings/test_light.py @@ -115,7 +115,7 @@ async def test_entity_and_device_attributes(hass, device_factory): assert entry assert entry.unique_id == device.device_id - entry = device_registry.async_get_device({(DOMAIN, device.device_id)}, []) + entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) assert entry assert entry.name == device.label assert entry.model == device.device_type_name diff --git a/tests/components/smartthings/test_lock.py b/tests/components/smartthings/test_lock.py index 65219852392..0492f2281ce 100644 --- a/tests/components/smartthings/test_lock.py +++ b/tests/components/smartthings/test_lock.py @@ -27,7 +27,7 @@ async def test_entity_and_device_attributes(hass, device_factory): assert entry assert entry.unique_id == device.device_id - entry = device_registry.async_get_device({(DOMAIN, device.device_id)}, []) + entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) assert entry assert entry.name == device.label assert entry.model == device.device_type_name diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index b9669d0c8ed..3faf0f621a3 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -85,7 +85,7 @@ async def test_entity_and_device_attributes(hass, device_factory): entry = entity_registry.async_get("sensor.sensor_1_battery") assert entry assert entry.unique_id == f"{device.device_id}.{Attribute.battery}" - entry = device_registry.async_get_device({(DOMAIN, device.device_id)}, []) + entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) assert entry assert entry.name == device.label assert entry.model == device.device_type_name diff --git a/tests/components/smartthings/test_smartapp.py b/tests/components/smartthings/test_smartapp.py index 42215def82f..7f26b26d577 100644 --- a/tests/components/smartthings/test_smartapp.py +++ b/tests/components/smartthings/test_smartapp.py @@ -1,4 +1,5 @@ """Tests for the smartapp module.""" +from unittest.mock import AsyncMock, Mock, patch from uuid import uuid4 from pysmartthings import CAPABILITIES, AppEntity, Capability @@ -10,7 +11,6 @@ from homeassistant.components.smartthings.const import ( DOMAIN, ) -from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index 0b47739caf5..3ac86426eeb 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -30,7 +30,7 @@ async def test_entity_and_device_attributes(hass, device_factory): assert entry assert entry.unique_id == device.device_id - entry = device_registry.async_get_device({(DOMAIN, device.device_id)}, []) + entry = device_registry.async_get_device({(DOMAIN, device.device_id)}) assert entry assert entry.name == device.label assert entry.model == device.device_type_name diff --git a/tests/components/smhi/common.py b/tests/components/smhi/common.py index 92c9e13fb8a..6f215840324 100644 --- a/tests/components/smhi/common.py +++ b/tests/components/smhi/common.py @@ -1,5 +1,5 @@ """Common test utilities.""" -from tests.async_mock import Mock +from unittest.mock import Mock class AsyncMock(Mock): diff --git a/tests/components/smhi/test_config_flow.py b/tests/components/smhi/test_config_flow.py index 56a0745c1b3..3f189b52311 100644 --- a/tests/components/smhi/test_config_flow.py +++ b/tests/components/smhi/test_config_flow.py @@ -1,11 +1,11 @@ """Tests for SMHI config flow.""" +from unittest.mock import Mock, patch + from smhi.smhi_lib import Smhi as SmhiApi, SmhiForecastException from homeassistant.components.smhi import config_flow from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE -from tests.async_mock import Mock, patch - # pylint: disable=protected-access async def test_homeassistant_location_exists() -> None: diff --git a/tests/components/smhi/test_init.py b/tests/components/smhi/test_init.py index e6b523d96bb..450ac7e6ef0 100644 --- a/tests/components/smhi/test_init.py +++ b/tests/components/smhi/test_init.py @@ -1,10 +1,10 @@ """Test SMHI component setup process.""" +from unittest.mock import Mock + from homeassistant.components import smhi from .common import AsyncMock -from tests.async_mock import Mock - TEST_CONFIG = { "config": { "name": "0123456789ABCDEF", diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index 050dc663487..9170f3a9ed0 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -2,6 +2,7 @@ import asyncio from datetime import datetime import logging +from unittest.mock import AsyncMock, Mock, patch from smhi.smhi_lib import APIURL_TEMPLATE, SmhiForecastException @@ -25,7 +26,6 @@ from homeassistant.components.weather import ( from homeassistant.const import TEMP_CELSIUS from homeassistant.core import HomeAssistant -from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry, load_fixture _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/smtp/test_notify.py b/tests/components/smtp/test_notify.py index 754df5945af..46f8d0efd5f 100644 --- a/tests/components/smtp/test_notify.py +++ b/tests/components/smtp/test_notify.py @@ -1,6 +1,7 @@ """The tests for the notify smtp platform.""" from os import path import re +from unittest.mock import patch import pytest @@ -11,8 +12,6 @@ from homeassistant.components.smtp.notify import MailNotificationService from homeassistant.const import SERVICE_RELOAD from homeassistant.setup import async_setup_component -from tests.async_mock import patch - class MockSMTP(MailNotificationService): """Test SMTP object that doesn't need a working server.""" diff --git a/tests/components/solaredge/test_config_flow.py b/tests/components/solaredge/test_config_flow.py index 835fc300982..4caae0edcfe 100644 --- a/tests/components/solaredge/test_config_flow.py +++ b/tests/components/solaredge/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for the SolarEdge config flow.""" +from unittest.mock import Mock, patch + import pytest from requests.exceptions import ConnectTimeout, HTTPError @@ -7,7 +9,6 @@ from homeassistant.components.solaredge import config_flow from homeassistant.components.solaredge.const import CONF_SITE_ID, DEFAULT_NAME from homeassistant.const import CONF_API_KEY, CONF_NAME -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry NAME = "solaredge site 1 2 3" diff --git a/tests/components/solarlog/test_config_flow.py b/tests/components/solarlog/test_config_flow.py index 8266adfd417..3016a73f1b8 100644 --- a/tests/components/solarlog/test_config_flow.py +++ b/tests/components/solarlog/test_config_flow.py @@ -1,4 +1,6 @@ """Test the solarlog config flow.""" +from unittest.mock import patch + import pytest from homeassistant import config_entries, data_entry_flow, setup @@ -6,7 +8,6 @@ from homeassistant.components.solarlog import config_flow from homeassistant.components.solarlog.const import DEFAULT_HOST, DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME -from tests.async_mock import patch from tests.common import MockConfigEntry NAME = "Solarlog test 1 2 3" diff --git a/tests/components/soma/test_config_flow.py b/tests/components/soma/test_config_flow.py index 929463ecf81..1d00f83a608 100644 --- a/tests/components/soma/test_config_flow.py +++ b/tests/components/soma/test_config_flow.py @@ -1,11 +1,12 @@ """Tests for the Soma config flow.""" +from unittest.mock import patch + from api.soma_api import SomaApi from requests import RequestException from homeassistant import data_entry_flow from homeassistant.components.soma import DOMAIN, config_flow -from tests.async_mock import patch from tests.common import MockConfigEntry MOCK_HOST = "123.45.67.89" diff --git a/tests/components/somfy/test_config_flow.py b/tests/components/somfy/test_config_flow.py index 4276a6a18d4..47adb5bdc91 100644 --- a/tests/components/somfy/test_config_flow.py +++ b/tests/components/somfy/test_config_flow.py @@ -1,5 +1,6 @@ """Tests for the Somfy config flow.""" import asyncio +from unittest.mock import patch import pytest @@ -8,7 +9,6 @@ from homeassistant.components.somfy import DOMAIN, config_flow from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow -from tests.async_mock import patch from tests.common import MockConfigEntry CLIENT_ID_VALUE = "1234" diff --git a/tests/components/somfy_mylink/__init__.py b/tests/components/somfy_mylink/__init__.py new file mode 100644 index 00000000000..b1141243997 --- /dev/null +++ b/tests/components/somfy_mylink/__init__.py @@ -0,0 +1 @@ +"""Tests for the Somfy MyLink integration.""" diff --git a/tests/components/somfy_mylink/test_config_flow.py b/tests/components/somfy_mylink/test_config_flow.py new file mode 100644 index 00000000000..980a01f318c --- /dev/null +++ b/tests/components/somfy_mylink/test_config_flow.py @@ -0,0 +1,525 @@ +"""Test the Somfy MyLink config flow.""" +import asyncio +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS +from homeassistant.components.somfy_mylink.const import ( + CONF_DEFAULT_REVERSE, + CONF_ENTITY_CONFIG, + CONF_REVERSE, + CONF_REVERSED_TARGET_IDS, + CONF_SYSTEM_ID, + DOMAIN, +) +from homeassistant.const import CONF_HOST, CONF_PORT + +from tests.common import MockConfigEntry + + +async def test_form_user(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", + return_value={"any": "data"}, + ), patch( + "homeassistant.components.somfy_mylink.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.somfy_mylink.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_PORT: 1234, + CONF_SYSTEM_ID: "456", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "MyLink 1.1.1.1" + assert result2["data"] == { + CONF_HOST: "1.1.1.1", + CONF_PORT: 1234, + CONF_SYSTEM_ID: "456", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_user_already_configured(hass): + """Test we abort if already configured.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "1.1.1.1", CONF_PORT: 12, CONF_SYSTEM_ID: 46}, + ) + config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", + return_value={"any": "data"}, + ), patch( + "homeassistant.components.somfy_mylink.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.somfy_mylink.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_PORT: 1234, + CONF_SYSTEM_ID: "456", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert len(mock_setup.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_form_import(hass): + """Test we get the form with import source.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", + return_value={"any": "data"}, + ), patch( + "homeassistant.components.somfy_mylink.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.somfy_mylink.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 1234, + CONF_SYSTEM_ID: 456, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == "MyLink 1.1.1.1" + assert result["data"] == { + CONF_HOST: "1.1.1.1", + CONF_PORT: 1234, + CONF_SYSTEM_ID: 456, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_import_with_entity_config(hass): + """Test we can import entity config.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", + return_value={"any": "data"}, + ), patch( + "homeassistant.components.somfy_mylink.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.somfy_mylink.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 1234, + CONF_SYSTEM_ID: 456, + CONF_DEFAULT_REVERSE: True, + CONF_ENTITY_CONFIG: {"cover.xyz": {CONF_REVERSE: False}}, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == "MyLink 1.1.1.1" + assert result["data"] == { + CONF_HOST: "1.1.1.1", + CONF_PORT: 1234, + CONF_SYSTEM_ID: 456, + CONF_DEFAULT_REVERSE: True, + CONF_ENTITY_CONFIG: {"cover.xyz": {CONF_REVERSE: False}}, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_import_already_exists(hass): + """Test we get the form with import source.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "1.1.1.1", CONF_PORT: 12, CONF_SYSTEM_ID: 46}, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", + return_value={"any": "data"}, + ), patch( + "homeassistant.components.somfy_mylink.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.somfy_mylink.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 1234, + CONF_SYSTEM_ID: "456", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert len(mock_setup.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_form_invalid_auth(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", + return_value={ + "jsonrpc": "2.0", + "error": {"code": -32652, "message": "Invalid auth"}, + "id": 818, + }, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_PORT: 1234, + CONF_SYSTEM_ID: "456", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", + side_effect=asyncio.TimeoutError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_PORT: 1234, + CONF_SYSTEM_ID: "456", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_unknown_error(hass): + """Test we handle broad exception.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", + side_effect=ValueError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_PORT: 1234, + CONF_SYSTEM_ID: "456", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} + + +async def test_options_not_loaded(hass): + """Test options will not display until loaded.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "1.1.1.1", CONF_PORT: 12, CONF_SYSTEM_ID: "46"}, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.somfy_mylink.SomfyMyLinkSynergy.status_info", + return_value={"result": []}, + ): + result = await hass.config_entries.options.async_init(config_entry.entry_id) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + + +@pytest.mark.parametrize("reversed", [True, False]) +async def test_options_with_targets(hass, reversed): + """Test we can configure reverse for a target.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "1.1.1.1", CONF_PORT: 12, CONF_SYSTEM_ID: "46"}, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.somfy_mylink.SomfyMyLinkSynergy.status_info", + return_value={ + "result": [ + { + "targetID": "a", + "name": "Master Window", + "type": 0, + } + ] + }, + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + result = await hass.config_entries.options.async_init(config_entry.entry_id) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"target_id": "a"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + result3 = await hass.config_entries.options.async_configure( + result2["flow_id"], + user_input={"reverse": reversed}, + ) + + assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + + result4 = await hass.config_entries.options.async_configure( + result3["flow_id"], + user_input={"target_id": None}, + ) + assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + assert config_entry.options == { + CONF_REVERSED_TARGET_IDS: {"a": reversed}, + } + + await hass.async_block_till_done() + + +@pytest.mark.parametrize("reversed", [True, False]) +async def test_form_import_with_entity_config_modify_options(hass, reversed): + """Test we can import entity config and modify options.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mock_imported_config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "1.1.1.1", + CONF_PORT: 1234, + CONF_SYSTEM_ID: "456", + CONF_DEFAULT_REVERSE: True, + CONF_ENTITY_CONFIG: {"cover.xyz": {CONF_REVERSE: False}}, + }, + ) + mock_imported_config_entry.add_to_hass(hass) + + mock_status_info = { + "result": [ + {"targetID": "1.1", "name": "xyz"}, + {"targetID": "1.2", "name": "zulu"}, + ] + } + + with patch( + "homeassistant.components.somfy_mylink.SomfyMyLinkSynergy.status_info", + return_value=mock_status_info, + ): + assert await hass.config_entries.async_setup( + mock_imported_config_entry.entry_id + ) + await hass.async_block_till_done() + + assert mock_imported_config_entry.options == { + "reversed_target_ids": {"1.2": True} + } + + result = await hass.config_entries.options.async_init( + mock_imported_config_entry.entry_id + ) + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"target_id": "1.2"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + result3 = await hass.config_entries.options.async_configure( + result2["flow_id"], + user_input={"reverse": reversed}, + ) + + assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM + + result4 = await hass.config_entries.options.async_configure( + result3["flow_id"], + user_input={"target_id": None}, + ) + assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + # Will not be altered if nothing changes + assert mock_imported_config_entry.options == { + CONF_REVERSED_TARGET_IDS: {"1.2": reversed}, + } + + await hass.async_block_till_done() + + +async def test_form_user_already_configured_from_dhcp(hass): + """Test we abort if already configured from dhcp.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: "1.1.1.1", CONF_PORT: 12, CONF_SYSTEM_ID: 46}, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", + return_value={"any": "data"}, + ), patch( + "homeassistant.components.somfy_mylink.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.somfy_mylink.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={ + IP_ADDRESS: "1.1.1.1", + MAC_ADDRESS: "AA:BB:CC:DD:EE:FF", + HOSTNAME: "somfy_eeff", + }, + ) + + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert len(mock_setup.mock_calls) == 0 + assert len(mock_setup_entry.mock_calls) == 0 + + +async def test_already_configured_with_ignored(hass): + """Test ignored entries do not break checking for existing entries.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + config_entry = MockConfigEntry(domain=DOMAIN, data={}, source="ignore") + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={ + IP_ADDRESS: "1.1.1.1", + MAC_ADDRESS: "AA:BB:CC:DD:EE:FF", + HOSTNAME: "somfy_eeff", + }, + ) + assert result["type"] == "form" + + +async def test_dhcp_discovery(hass): + """Test we can process the discovery from dhcp.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={ + IP_ADDRESS: "1.1.1.1", + MAC_ADDRESS: "AA:BB:CC:DD:EE:FF", + HOSTNAME: "somfy_eeff", + }, + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", + return_value={"any": "data"}, + ), patch( + "homeassistant.components.somfy_mylink.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.somfy_mylink.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOST: "1.1.1.1", + CONF_PORT: 1234, + CONF_SYSTEM_ID: "456", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "MyLink 1.1.1.1" + assert result2["data"] == { + CONF_HOST: "1.1.1.1", + CONF_PORT: 1234, + CONF_SYSTEM_ID: "456", + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/sonarr/__init__.py b/tests/components/sonarr/__init__.py index 61afac099d3..1313db4460d 100644 --- a/tests/components/sonarr/__init__.py +++ b/tests/components/sonarr/__init__.py @@ -1,5 +1,6 @@ """Tests for the Sonarr component.""" from socket import gaierror as SocketGIAError +from unittest.mock import patch from homeassistant.components.sonarr.const import ( CONF_BASE_PATH, @@ -19,7 +20,6 @@ from homeassistant.const import ( ) from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import patch from tests.common import MockConfigEntry, load_fixture from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/sonarr/test_config_flow.py b/tests/components/sonarr/test_config_flow.py index f872f7f8c18..701580ab37c 100644 --- a/tests/components/sonarr/test_config_flow.py +++ b/tests/components/sonarr/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Sonarr config flow.""" +from unittest.mock import patch + from homeassistant.components.sonarr.const import ( CONF_UPCOMING_DAYS, CONF_WANTED_MAX_ITEMS, @@ -15,7 +17,6 @@ from homeassistant.data_entry_flow import ( ) from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import patch from tests.components.sonarr import ( HOST, MOCK_REAUTH_INPUT, diff --git a/tests/components/sonarr/test_init.py b/tests/components/sonarr/test_init.py index 258be0203bb..16d33a23072 100644 --- a/tests/components/sonarr/test_init.py +++ b/tests/components/sonarr/test_init.py @@ -1,4 +1,6 @@ """Tests for the Sonsrr integration.""" +from unittest.mock import patch + from homeassistant.components.sonarr.const import DOMAIN from homeassistant.config_entries import ( ENTRY_STATE_LOADED, @@ -10,7 +12,6 @@ from homeassistant.config_entries import ( from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant -from tests.async_mock import patch from tests.components.sonarr import setup_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/sonarr/test_sensor.py b/tests/components/sonarr/test_sensor.py index 94230c9e726..0b306dc8240 100644 --- a/tests/components/sonarr/test_sensor.py +++ b/tests/components/sonarr/test_sensor.py @@ -1,5 +1,6 @@ """Tests for the Sonarr sensor platform.""" from datetime import timedelta +from unittest.mock import patch import pytest @@ -14,7 +15,6 @@ from homeassistant.const import ( from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import dt as dt_util -from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.sonarr import mock_connection, setup_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/songpal/__init__.py b/tests/components/songpal/__init__.py index bca879268cc..f3004ef22e2 100644 --- a/tests/components/songpal/__init__.py +++ b/tests/components/songpal/__init__.py @@ -1,11 +1,11 @@ """Test the songpal integration.""" +from unittest.mock import AsyncMock, MagicMock, patch + from songpal import SongpalException from homeassistant.components.songpal.const import CONF_ENDPOINT from homeassistant.const import CONF_NAME -from tests.async_mock import AsyncMock, MagicMock, patch - FRIENDLY_NAME = "name" ENTITY_ID = f"media_player.{FRIENDLY_NAME}" HOST = "0.0.0.0" diff --git a/tests/components/songpal/test_config_flow.py b/tests/components/songpal/test_config_flow.py index 155801dca03..a1751bca676 100644 --- a/tests/components/songpal/test_config_flow.py +++ b/tests/components/songpal/test_config_flow.py @@ -1,5 +1,6 @@ """Test the songpal config flow.""" import copy +from unittest.mock import patch from homeassistant.components import ssdp from homeassistant.components.songpal.const import CONF_ENDPOINT, DOMAIN @@ -21,7 +22,6 @@ from . import ( _patch_config_flow_device, ) -from tests.async_mock import patch from tests.common import MockConfigEntry UDN = "uuid:1234" diff --git a/tests/components/songpal/test_init.py b/tests/components/songpal/test_init.py index 9f5de326cc0..8efcab4148b 100644 --- a/tests/components/songpal/test_init.py +++ b/tests/components/songpal/test_init.py @@ -1,4 +1,6 @@ """Tests songpal setup.""" +from unittest.mock import patch + from homeassistant.components import songpal from homeassistant.setup import async_setup_component @@ -9,7 +11,6 @@ from . import ( _patch_media_player_device, ) -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/songpal/test_media_player.py b/tests/components/songpal/test_media_player.py index 61b59ee1b56..aff79ef62ff 100644 --- a/tests/components/songpal/test_media_player.py +++ b/tests/components/songpal/test_media_player.py @@ -1,6 +1,7 @@ """Test songpal media_player.""" from datetime import timedelta import logging +from unittest.mock import AsyncMock, MagicMock, call, patch from songpal import ( ConnectChange, @@ -32,7 +33,6 @@ from . import ( _patch_media_player_device, ) -from tests.async_mock import AsyncMock, MagicMock, call, patch from tests.common import MockConfigEntry, async_fire_time_changed @@ -114,9 +114,7 @@ async def test_state(hass): assert attributes["supported_features"] == SUPPORT_SONGPAL device_registry = await dr.async_get_registry(hass) - device = device_registry.async_get_device( - identifiers={(songpal.DOMAIN, MAC)}, connections={} - ) + device = device_registry.async_get_device(identifiers={(songpal.DOMAIN, MAC)}) assert device.connections == {(dr.CONNECTION_NETWORK_MAC, MAC)} assert device.manufacturer == "Sony Corporation" assert device.name == FRIENDLY_NAME diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index f6249d0bc81..1ce2205813b 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -1,11 +1,12 @@ """Configuration for Sonos tests.""" +from unittest.mock import Mock, patch as patch + import pytest from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.sonos import DOMAIN from homeassistant.const import CONF_HOSTS -from tests.async_mock import Mock, patch as patch from tests.common import MockConfigEntry diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index dd83aefba81..6a401ee0c16 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -50,8 +50,7 @@ async def test_device_registry(hass, config_entry, config, soco): device_registry = await hass.helpers.device_registry.async_get_registry() reg_device = device_registry.async_get_device( - identifiers={("sonos", "RINCON_test")}, - connections=set(), + identifiers={("sonos", "RINCON_test")} ) assert reg_device.model == "Model Name" assert reg_device.sw_version == "49.2-64250" diff --git a/tests/components/sonos/test_plex_playback.py b/tests/components/sonos/test_plex_playback.py new file mode 100644 index 00000000000..f9bedbfe1f7 --- /dev/null +++ b/tests/components/sonos/test_plex_playback.py @@ -0,0 +1,67 @@ +"""Tests for the Sonos Media Player platform.""" +from unittest.mock import patch + +import pytest + +from homeassistant.components.media_player.const import ( + ATTR_MEDIA_CONTENT_ID, + ATTR_MEDIA_CONTENT_TYPE, + DOMAIN as MP_DOMAIN, + MEDIA_TYPE_MUSIC, + SERVICE_PLAY_MEDIA, +) +from homeassistant.components.plex.const import PLEX_URI_SCHEME +from homeassistant.const import ATTR_ENTITY_ID +from homeassistant.exceptions import HomeAssistantError + +from .test_media_player import setup_platform + + +async def test_plex_play_media( + hass, + config_entry, + config, +): + """Test playing media via the Plex integration.""" + await setup_platform(hass, config_entry, config) + media_player = "media_player.zone_a" + media_content_id = ( + '{"library_name": "Music", "artist_name": "Artist", "album_name": "Album"}' + ) + + with patch( + "homeassistant.components.sonos.media_player.play_on_sonos" + ) as mock_play: + # Test successful Plex service call + assert await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: f"{PLEX_URI_SCHEME}{media_content_id}", + }, + blocking=True, + ) + + assert len(mock_play.mock_calls) == 1 + assert mock_play.mock_calls[0][1][1] == MEDIA_TYPE_MUSIC + assert mock_play.mock_calls[0][1][2] == media_content_id + assert mock_play.mock_calls[0][1][3] == "Zone A" + + # Test failed Plex service call + mock_play.reset_mock() + mock_play.side_effect = HomeAssistantError + + with pytest.raises(HomeAssistantError): + await hass.services.async_call( + MP_DOMAIN, + SERVICE_PLAY_MEDIA, + { + ATTR_ENTITY_ID: media_player, + ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_MUSIC, + ATTR_MEDIA_CONTENT_ID: f"{PLEX_URI_SCHEME}{media_content_id}", + }, + blocking=True, + ) + assert mock_play.called diff --git a/tests/components/soundtouch/test_media_player.py b/tests/components/soundtouch/test_media_player.py index 85089854c4d..4ed8a648c77 100644 --- a/tests/components/soundtouch/test_media_player.py +++ b/tests/components/soundtouch/test_media_player.py @@ -1,4 +1,6 @@ """Test the Soundtouch component.""" +from unittest.mock import call, patch + from libsoundtouch.device import ( Config, Preset, @@ -26,8 +28,6 @@ from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.helpers.discovery import async_load_platform from homeassistant.setup import async_setup_component -from tests.async_mock import call, patch - # pylint: disable=super-init-not-called diff --git a/tests/components/speedtestdotnet/test_config_flow.py b/tests/components/speedtestdotnet/test_config_flow.py index d421113a2d7..dee271d94a3 100644 --- a/tests/components/speedtestdotnet/test_config_flow.py +++ b/tests/components/speedtestdotnet/test_config_flow.py @@ -1,5 +1,6 @@ """Tests for SpeedTest config flow.""" from datetime import timedelta +from unittest.mock import patch import pytest from speedtest import NoMatchedServers @@ -17,7 +18,6 @@ from homeassistant.const import CONF_MONITORED_CONDITIONS, CONF_SCAN_INTERVAL from . import MOCK_SERVERS -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/speedtestdotnet/test_init.py b/tests/components/speedtestdotnet/test_init.py index cadf97fc761..72bcb743a8d 100644 --- a/tests/components/speedtestdotnet/test_init.py +++ b/tests/components/speedtestdotnet/test_init.py @@ -1,11 +1,12 @@ """Tests for SpeedTest integration.""" +from unittest.mock import patch + import speedtest from homeassistant import config_entries from homeassistant.components import speedtestdotnet from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/speedtestdotnet/test_sensor.py b/tests/components/speedtestdotnet/test_sensor.py index 5c1606f0f4b..c08a9f3304f 100644 --- a/tests/components/speedtestdotnet/test_sensor.py +++ b/tests/components/speedtestdotnet/test_sensor.py @@ -1,11 +1,12 @@ """Tests for SpeedTest sensors.""" +from unittest.mock import patch + from homeassistant.components import speedtestdotnet from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.speedtestdotnet.const import DEFAULT_NAME, SENSOR_TYPES from . import MOCK_RESULTS, MOCK_SERVERS, MOCK_STATES -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/spider/test_config_flow.py b/tests/components/spider/test_config_flow.py index ca1b37434d1..e8d10b51cf3 100644 --- a/tests/components/spider/test_config_flow.py +++ b/tests/components/spider/test_config_flow.py @@ -1,11 +1,12 @@ """Tests for the Spider config flow.""" +from unittest.mock import Mock, patch + import pytest from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.spider.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry USERNAME = "spider-username" diff --git a/tests/components/spotify/test_config_flow.py b/tests/components/spotify/test_config_flow.py index 53e87e5bdae..37a33ef66b2 100644 --- a/tests/components/spotify/test_config_flow.py +++ b/tests/components/spotify/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for the Spotify config flow.""" +from unittest.mock import patch + from spotipy import SpotifyException from homeassistant import data_entry_flow, setup @@ -7,7 +9,6 @@ from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/squeezebox/test_config_flow.py b/tests/components/squeezebox/test_config_flow.py index f325024cf00..2dae2f78d01 100644 --- a/tests/components/squeezebox/test_config_flow.py +++ b/tests/components/squeezebox/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Logitech Squeezebox config flow.""" +from unittest.mock import patch + from pysqueezebox import Server from homeassistant import config_entries @@ -16,7 +18,6 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) -from tests.async_mock import patch from tests.common import MockConfigEntry HOST = "1.1.1.1" diff --git a/tests/components/srp_energy/__init__.py b/tests/components/srp_energy/__init__.py index 34f06e2993e..5e2a4695d0b 100644 --- a/tests/components/srp_energy/__init__.py +++ b/tests/components/srp_energy/__init__.py @@ -1,9 +1,10 @@ """Tests for the SRP Energy integration.""" +from unittest.mock import patch + from homeassistant import config_entries from homeassistant.components import srp_energy from homeassistant.const import CONF_ID, CONF_NAME, CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry ENTRY_OPTIONS = {} diff --git a/tests/components/srp_energy/test_config_flow.py b/tests/components/srp_energy/test_config_flow.py index acb9d28f75d..c63843723b1 100644 --- a/tests/components/srp_energy/test_config_flow.py +++ b/tests/components/srp_energy/test_config_flow.py @@ -1,11 +1,11 @@ """Test the SRP Energy config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, data_entry_flow from homeassistant.components.srp_energy.const import CONF_IS_TOU, SRP_ENERGY_DOMAIN from . import ENTRY_CONFIG, init_integration -from tests.async_mock import patch - async def test_form(hass): """Test user config.""" @@ -18,7 +18,14 @@ async def test_form(hass): assert result["errors"] == {} # Fill submit form data for config entry - with patch("homeassistant.components.srp_energy.config_flow.SrpEnergyClient"): + with patch( + "homeassistant.components.srp_energy.config_flow.SrpEnergyClient" + ), patch( + "homeassistant.components.srp_energy.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.srp_energy.async_setup_entry", + return_value=True, + ) as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -29,6 +36,9 @@ async def test_form(hass): assert result["title"] == "Test" assert result["data"][CONF_IS_TOU] is False + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + async def test_form_invalid_auth(hass): """Test user config with invalid auth.""" diff --git a/tests/components/srp_energy/test_sensor.py b/tests/components/srp_energy/test_sensor.py index 3a70a3ec09f..a93e56b7b93 100644 --- a/tests/components/srp_energy/test_sensor.py +++ b/tests/components/srp_energy/test_sensor.py @@ -1,4 +1,6 @@ """Tests for the srp_energy sensor platform.""" +from unittest.mock import MagicMock + from homeassistant.components.srp_energy.const import ( ATTRIBUTION, DEFAULT_NAME, @@ -10,8 +12,6 @@ from homeassistant.components.srp_energy.const import ( from homeassistant.components.srp_energy.sensor import SrpEntity, async_setup_entry from homeassistant.const import ATTR_ATTRIBUTION, ENERGY_KILO_WATT_HOUR -from tests.async_mock import MagicMock - async def test_async_setup_entry(hass): """Test the sensor.""" diff --git a/tests/components/ssdp/test_init.py b/tests/components/ssdp/test_init.py index 008995cd78d..bba809aedbb 100644 --- a/tests/components/ssdp/test_init.py +++ b/tests/components/ssdp/test_init.py @@ -170,3 +170,46 @@ async def test_scan_description_parse_fail(hass, aioclient_mock): return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})], ): await scanner.async_scan(None) + + +async def test_invalid_characters(hass, aioclient_mock): + """Test that we replace bad characters with placeholders.""" + aioclient_mock.get( + "http://1.1.1.1", + text=""" + + + ABC + \xff\xff\xff\xff + + + """, + ) + scanner = ssdp.Scanner( + hass, + { + "mock-domain": [ + { + ssdp.ATTR_UPNP_DEVICE_TYPE: "ABC", + } + ] + }, + ) + + with patch( + "netdisco.ssdp.scan", + return_value=[Mock(st="mock-st", location="http://1.1.1.1", values={})], + ), patch.object( + hass.config_entries.flow, "async_init", return_value=mock_coro() + ) as mock_init: + await scanner.async_scan(None) + + assert len(mock_init.mock_calls) == 1 + assert mock_init.mock_calls[0][1][0] == "mock-domain" + assert mock_init.mock_calls[0][2]["context"] == {"source": "ssdp"} + assert mock_init.mock_calls[0][2]["data"] == { + "ssdp_location": "http://1.1.1.1", + "ssdp_st": "mock-st", + "deviceType": "ABC", + "serialNumber": "ÿÿÿÿ", + } diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 24401963974..b6bebcfeeda 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -3,6 +3,7 @@ from datetime import datetime, timedelta from os import path import statistics import unittest +from unittest.mock import patch import pytest @@ -18,7 +19,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component, setup_component from homeassistant.util import dt as dt_util -from tests.async_mock import patch from tests.common import ( fire_time_changed, get_test_home_assistant, diff --git a/tests/components/statsd/test_init.py b/tests/components/statsd/test_init.py index 30b52659685..17e49b09951 100644 --- a/tests/components/statsd/test_init.py +++ b/tests/components/statsd/test_init.py @@ -1,5 +1,6 @@ """The tests for the StatsD feeder.""" from unittest import mock +from unittest.mock import MagicMock, patch import pytest import voluptuous as vol @@ -9,8 +10,6 @@ from homeassistant.const import EVENT_STATE_CHANGED, STATE_OFF, STATE_ON import homeassistant.core as ha from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, patch - @pytest.fixture def mock_client(): diff --git a/tests/components/stream/conftest.py b/tests/components/stream/conftest.py new file mode 100644 index 00000000000..1b2f0645f9b --- /dev/null +++ b/tests/components/stream/conftest.py @@ -0,0 +1,60 @@ +"""Test fixtures for the stream component. + +The tests encode stream (as an h264 video), then load the stream and verify +that it is decoded properly. The background worker thread responsible for +decoding will decode the stream as fast as possible, and when completed +clears all output buffers. This can be a problem for the test that wishes +to retrieve and verify decoded segments. If the worker finishes first, there is +nothing for the test to verify. The solution is the WorkerSync class that +allows the tests to pause the worker thread before finalizing the stream +so that it can inspect the output. +""" + +import logging +import threading +from unittest.mock import patch + +import pytest + +from homeassistant.components.stream.core import Segment, StreamOutput + + +class WorkerSync: + """Test fixture that intercepts stream worker calls to StreamOutput.""" + + def __init__(self): + """Initialize WorkerSync.""" + self._event = None + self._put_original = StreamOutput.put + + def pause(self): + """Pause the worker before it finalizes the stream.""" + self._event = threading.Event() + + def resume(self): + """Allow the worker thread to finalize the stream.""" + self._event.set() + + def blocking_put(self, stream_output: StreamOutput, segment: Segment): + """Proxy StreamOutput.put, intercepted for test to pause worker.""" + if segment is None and self._event: + # Worker is ending the stream, which clears all output buffers. + # Block the worker thread until the test has a chance to verify + # the segments under test. + logging.error("blocking worker") + self._event.wait() + + # Forward to actual StreamOutput.put + self._put_original(stream_output, segment) + + +@pytest.fixture() +def stream_worker_sync(hass): + """Patch StreamOutput to allow test to synchronize worker stream end.""" + sync = WorkerSync() + with patch( + "homeassistant.components.stream.core.StreamOutput.put", + side_effect=sync.blocking_put, + autospec=True, + ): + yield sync diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index 16d2d724f22..790222b1630 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -1,22 +1,20 @@ """The tests for hls streams.""" from datetime import timedelta +from unittest.mock import patch from urllib.parse import urlparse import av -import pytest from homeassistant.components.stream import request_stream from homeassistant.const import HTTP_NOT_FOUND from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.stream.common import generate_h264_video, preload_stream -@pytest.mark.skip("Flaky in CI") -async def test_hls_stream(hass, hass_client): +async def test_hls_stream(hass, hass_client, stream_worker_sync): """ Test hls stream. @@ -25,6 +23,8 @@ async def test_hls_stream(hass, hass_client): """ await async_setup_component(hass, "stream", {"stream": {}}) + stream_worker_sync.pause() + # Setup demo HLS track source = generate_h264_video() stream = preload_stream(hass, source) @@ -50,10 +50,12 @@ async def test_hls_stream(hass, hass_client): # Fetch segment playlist = await playlist_response.text() playlist_url = "/".join(parsed_url.path.split("/")[:-1]) - segment_url = playlist_url + playlist.splitlines()[-1][1:] + segment_url = playlist_url + "/" + playlist.splitlines()[-1] segment_response = await http_client.get(segment_url) assert segment_response.status == 200 + stream_worker_sync.resume() + # Stop stream, if it hasn't quit already stream.stop() @@ -62,11 +64,12 @@ async def test_hls_stream(hass, hass_client): assert fail_response.status == HTTP_NOT_FOUND -@pytest.mark.skip("Flaky in CI") -async def test_stream_timeout(hass, hass_client): +async def test_stream_timeout(hass, hass_client, stream_worker_sync): """Test hls stream timeout.""" await async_setup_component(hass, "stream", {"stream": {}}) + stream_worker_sync.pause() + # Setup demo HLS track source = generate_h264_video() stream = preload_stream(hass, source) @@ -90,6 +93,8 @@ async def test_stream_timeout(hass, hass_client): playlist_response = await http_client.get(parsed_url.path) assert playlist_response.status == 200 + stream_worker_sync.resume() + # Wait 5 minutes future = dt_util.utcnow() + timedelta(minutes=5) async_fire_time_changed(hass, future) @@ -99,11 +104,12 @@ async def test_stream_timeout(hass, hass_client): assert fail_response.status == HTTP_NOT_FOUND -@pytest.mark.skip("Flaky in CI") -async def test_stream_ended(hass): +async def test_stream_ended(hass, stream_worker_sync): """Test hls stream packets ended.""" await async_setup_component(hass, "stream", {"stream": {}}) + stream_worker_sync.pause() + # Setup demo HLS track source = generate_h264_video() stream = preload_stream(hass, source) @@ -118,6 +124,9 @@ async def test_stream_ended(hass): if segment is None: break segments = segment.sequence + # Allow worker to finalize once enough of the stream is been consumed + if segments > 1: + stream_worker_sync.resume() assert segments > 1 assert not track.get_segment() diff --git a/tests/components/stream/test_init.py b/tests/components/stream/test_init.py index 505de7ca018..1515ff1a490 100644 --- a/tests/components/stream/test_init.py +++ b/tests/components/stream/test_init.py @@ -1,4 +1,6 @@ """The tests for stream.""" +from unittest.mock import AsyncMock, MagicMock, patch + import pytest from homeassistant.components.stream.const import ( @@ -12,8 +14,6 @@ from homeassistant.const import CONF_FILENAME from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, MagicMock, patch - async def test_record_service_invalid_file(hass): """Test record service call with invalid file.""" diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index cb6a1c9d36f..1b46738c8f2 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -1,6 +1,9 @@ """The tests for hls streams.""" from datetime import timedelta -from io import BytesIO +import logging +import os +import threading +from unittest.mock import patch import av import pytest @@ -10,44 +13,99 @@ from homeassistant.components.stream.recorder import recorder_save_worker from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import async_fire_time_changed from tests.components.stream.common import generate_h264_video, preload_stream +TEST_TIMEOUT = 10 -@pytest.mark.skip("Flaky in CI") -async def test_record_stream(hass, hass_client): + +class SaveRecordWorkerSync: + """ + Test fixture to manage RecordOutput thread for recorder_save_worker. + + This is used to assert that the worker is started and stopped cleanly + to avoid thread leaks in tests. + """ + + def __init__(self): + """Initialize SaveRecordWorkerSync.""" + self.reset() + + def recorder_save_worker(self, *args, **kwargs): + """Mock method for patch.""" + logging.debug("recorder_save_worker thread started") + assert self._save_thread is None + self._save_thread = threading.current_thread() + self._save_event.set() + + def join(self): + """Verify save worker was invoked and block on shutdown.""" + assert self._save_event.wait(timeout=TEST_TIMEOUT) + self._save_thread.join() + + def reset(self): + """Reset callback state for reuse in tests.""" + self._save_thread = None + self._save_event = threading.Event() + + +@pytest.fixture() +def record_worker_sync(hass): + """Patch recorder_save_worker for clean thread shutdown for test.""" + sync = SaveRecordWorkerSync() + with patch( + "homeassistant.components.stream.recorder.recorder_save_worker", + side_effect=sync.recorder_save_worker, + autospec=True, + ): + yield sync + + +async def test_record_stream(hass, hass_client, stream_worker_sync, record_worker_sync): """ Test record stream. - Purposefully not mocking anything here to test full - integration with the stream component. + Tests full integration with the stream component, and captures the + stream worker and save worker to allow for clean shutdown of background + threads. The actual save logic is tested in test_recorder_save below. """ await async_setup_component(hass, "stream", {"stream": {}}) - with patch("homeassistant.components.stream.recorder.recorder_save_worker"): - # Setup demo track - source = generate_h264_video() - stream = preload_stream(hass, source) - recorder = stream.add_provider("recorder") - stream.start() + stream_worker_sync.pause() - while True: - segment = await recorder.recv() - if not segment: - break - segments = segment.sequence + # Setup demo track + source = generate_h264_video() + stream = preload_stream(hass, source) + recorder = stream.add_provider("recorder") + stream.start() - stream.stop() + while True: + segment = await recorder.recv() + if not segment: + break + segments = segment.sequence + if segments > 1: + stream_worker_sync.resume() - assert segments > 1 + stream.stop() + assert segments > 1 + + # Verify that the save worker was invoked, then block until its + # thread completes and is shutdown completely to avoid thread leaks. + record_worker_sync.join() -@pytest.mark.skip("Flaky in CI") -async def test_recorder_timeout(hass, hass_client): - """Test recorder timeout.""" +async def test_recorder_timeout(hass, hass_client, stream_worker_sync): + """ + Test recorder timeout. + + Mocks out the cleanup to assert that it is invoked after a timeout. + This test does not start the recorder save thread. + """ await async_setup_component(hass, "stream", {"stream": {}}) + stream_worker_sync.pause() + with patch( "homeassistant.components.stream.recorder.RecorderOutput.cleanup" ) as mock_cleanup: @@ -66,24 +124,28 @@ async def test_recorder_timeout(hass, hass_client): assert mock_cleanup.called + stream_worker_sync.resume() + stream.stop() + await hass.async_block_till_done() + await hass.async_block_till_done() -@pytest.mark.skip("Flaky in CI") -async def test_recorder_save(): + +async def test_recorder_save(tmpdir): """Test recorder save.""" # Setup source = generate_h264_video() - output = BytesIO() - output.name = "test.mp4" + filename = f"{tmpdir}/test.mp4" # Run - recorder_save_worker(output, [Segment(1, source, 4)], "mp4") + recorder_save_worker(filename, [Segment(1, source, 4)], "mp4") # Assert - assert output.getvalue() + assert os.path.exists(filename) -@pytest.mark.skip("Flaky in CI") -async def test_record_stream_audio(hass, hass_client): +async def test_record_stream_audio( + hass, hass_client, stream_worker_sync, record_worker_sync +): """ Test treatment of different audio inputs. @@ -98,23 +160,31 @@ async def test_record_stream_audio(hass, hass_client): ("empty", 0), # audio stream with no packets (None, 0), # no audio stream ): - with patch("homeassistant.components.stream.recorder.recorder_save_worker"): - # Setup demo track - source = generate_h264_video( - container_format="mov", audio_codec=a_codec - ) # mov can store PCM - stream = preload_stream(hass, source) - recorder = stream.add_provider("recorder") - stream.start() + record_worker_sync.reset() + stream_worker_sync.pause() - while True: - segment = await recorder.recv() - if not segment: - break - last_segment = segment + # Setup demo track + source = generate_h264_video( + container_format="mov", audio_codec=a_codec + ) # mov can store PCM + stream = preload_stream(hass, source) + recorder = stream.add_provider("recorder") + stream.start() - result = av.open(last_segment.segment, "r", format="mp4") + while True: + segment = await recorder.recv() + if not segment: + break + last_segment = segment + stream_worker_sync.resume() - assert len(result.streams.audio) == expected_audio_streams - result.close() - stream.stop() + result = av.open(last_segment.segment, "r", format="mp4") + + assert len(result.streams.audio) == expected_audio_streams + result.close() + stream.stop() + await hass.async_block_till_done() + + # Verify that the save worker was invoked, then block until its + # thread completes and is shutdown completely to avoid thread leaks. + record_worker_sync.join() diff --git a/tests/components/sun/test_init.py b/tests/components/sun/test_init.py index 36eaa8398ac..1e95082b358 100644 --- a/tests/components/sun/test_init.py +++ b/tests/components/sun/test_init.py @@ -1,5 +1,6 @@ """The tests for the Sun component.""" from datetime import datetime, timedelta +from unittest.mock import patch from pytest import mark @@ -9,8 +10,6 @@ import homeassistant.core as ha from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch - async def test_setting_rising(hass, legacy_patchable_time): """Test retrieving sun setting and rising.""" diff --git a/tests/components/sun/test_trigger.py b/tests/components/sun/test_trigger.py index 1a3de56964c..a288150517d 100644 --- a/tests/components/sun/test_trigger.py +++ b/tests/components/sun/test_trigger.py @@ -1,5 +1,6 @@ """The tests for the sun automation.""" from datetime import datetime +from unittest.mock import patch import pytest @@ -16,7 +17,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import async_fire_time_changed, async_mock_service, mock_component from tests.components.blueprint.conftest import stub_blueprint_populate # noqa diff --git a/tests/components/surepetcare/__init__.py b/tests/components/surepetcare/__init__.py index a7ce6d3b6a6..d4af323d063 100644 --- a/tests/components/surepetcare/__init__.py +++ b/tests/components/surepetcare/__init__.py @@ -1,9 +1,9 @@ """Tests for Sure Petcare integration.""" +from unittest.mock import patch + from homeassistant.components.surepetcare.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import patch - HOUSEHOLD_ID = "household-id" HUB_ID = "hub-id" diff --git a/tests/components/surepetcare/conftest.py b/tests/components/surepetcare/conftest.py index ed42fe2532b..44e2a722406 100644 --- a/tests/components/surepetcare/conftest.py +++ b/tests/components/surepetcare/conftest.py @@ -1,11 +1,11 @@ """Define fixtures available for all tests.""" +from unittest.mock import AsyncMock, patch + from pytest import fixture from surepy import SurePetcare from homeassistant.helpers.aiohttp_client import async_get_clientsession -from tests.async_mock import AsyncMock, patch - @fixture async def surepetcare(hass): @@ -18,6 +18,5 @@ async def surepetcare(hass): async_get_clientsession(hass), api_timeout=1, ) - instance.get_data = AsyncMock(return_value=None) - + instance._get_resource = AsyncMock(return_value=None) yield mock_surepetcare diff --git a/tests/components/surepetcare/test_binary_sensor.py b/tests/components/surepetcare/test_binary_sensor.py index 9478ca7a1d4..ad723f707f5 100644 --- a/tests/components/surepetcare/test_binary_sensor.py +++ b/tests/components/surepetcare/test_binary_sensor.py @@ -1,4 +1,6 @@ """The tests for the Sure Petcare binary sensor platform.""" +from surepy import MESTART_RESOURCE + from homeassistant.components.surepetcare.const import DOMAIN from homeassistant.setup import async_setup_component @@ -16,8 +18,7 @@ EXPECTED_ENTITY_IDS = { async def test_binary_sensors(hass, surepetcare) -> None: """Test the generation of unique ids.""" instance = surepetcare.return_value - instance.data = MOCK_API_DATA - instance.get_data.return_value = MOCK_API_DATA + instance._resource[MESTART_RESOURCE] = {"data": MOCK_API_DATA} with _patch_sensor_setup(): assert await async_setup_component(hass, DOMAIN, MOCK_CONFIG) diff --git a/tests/components/switch/test_device_condition.py b/tests/components/switch/test_device_condition.py index 093758cbe62..67ba3e8e38e 100644 --- a/tests/components/switch/test_device_condition.py +++ b/tests/components/switch/test_device_condition.py @@ -1,5 +1,6 @@ """The test for switch device automation.""" from datetime import timedelta +from unittest.mock import patch import pytest @@ -10,7 +11,6 @@ from homeassistant.helpers import device_registry from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import ( MockConfigEntry, async_get_device_automation_capabilities, diff --git a/tests/components/switch/test_significant_change.py b/tests/components/switch/test_significant_change.py new file mode 100644 index 00000000000..194efb68583 --- /dev/null +++ b/tests/components/switch/test_significant_change.py @@ -0,0 +1,12 @@ +"""Test the sensor significant change platform.""" +from homeassistant.components.switch.significant_change import ( + async_check_significant_change, +) + + +async def test_significant_change(): + """Detect Switch significant change.""" + attrs = {} + assert not async_check_significant_change(None, "on", attrs, "on", attrs) + assert not async_check_significant_change(None, "off", attrs, "off", attrs) + assert async_check_significant_change(None, "on", attrs, "off", attrs) diff --git a/tests/components/switcher_kis/conftest.py b/tests/components/switcher_kis/conftest.py index 37d61d6ca19..e7303a20ea5 100644 --- a/tests/components/switcher_kis/conftest.py +++ b/tests/components/switcher_kis/conftest.py @@ -3,6 +3,7 @@ from asyncio import Queue from datetime import datetime from typing import Any, Generator, Optional +from unittest.mock import AsyncMock, patch from pytest import fixture @@ -10,6 +11,7 @@ from .consts import ( DUMMY_AUTO_OFF_SET, DUMMY_DEVICE_ID, DUMMY_DEVICE_NAME, + DUMMY_DEVICE_PASSWORD, DUMMY_DEVICE_STATE, DUMMY_ELECTRIC_CURRENT, DUMMY_IP_ADDRESS, @@ -19,8 +21,6 @@ from .consts import ( DUMMY_REMAINING_TIME, ) -from tests.async_mock import AsyncMock, patch - @patch("aioswitcher.devices.SwitcherV2Device") class MockSwitcherV2Device: @@ -80,6 +80,11 @@ class MockSwitcherV2Device: """Return the phone id.""" return DUMMY_PHONE_ID + @property + def device_password(self) -> str: + """Return the device password.""" + return DUMMY_DEVICE_PASSWORD + @property def last_data_update(self) -> datetime: """Return the timestamp of the last update.""" @@ -170,10 +175,11 @@ def mock_api_fixture() -> Generator[AsyncMock, Any, None]: patchers = [ patch( - "homeassistant.components.switcher_kis.SwitcherV2Api.connect", new=mock_api + "homeassistant.components.switcher_kis.switch.SwitcherV2Api.connect", + new=mock_api, ), patch( - "homeassistant.components.switcher_kis.SwitcherV2Api.disconnect", + "homeassistant.components.switcher_kis.switch.SwitcherV2Api.disconnect", new=mock_api, ), ] diff --git a/tests/components/switcher_kis/consts.py b/tests/components/switcher_kis/consts.py index b2e312183a7..ab5951710f4 100644 --- a/tests/components/switcher_kis/consts.py +++ b/tests/components/switcher_kis/consts.py @@ -8,6 +8,7 @@ from homeassistant.components.switcher_kis import ( ) DUMMY_AUTO_OFF_SET = "01:30:00" +DUMMY_TIMER_MINUTES_SET = "90" DUMMY_DEVICE_ID = "a123bc" DUMMY_DEVICE_NAME = "Device Name" DUMMY_DEVICE_PASSWORD = "12345678" @@ -22,7 +23,7 @@ DUMMY_POWER_CONSUMPTION = 2780 DUMMY_REMAINING_TIME = "01:29:32" # Adjust if any modification were made to DUMMY_DEVICE_NAME -SWITCH_ENTITY_ID = "switch.switcher_kis_device_name" +SWITCH_ENTITY_ID = "switch.device_name" MANDATORY_CONFIGURATION = { DOMAIN: { diff --git a/tests/components/switcher_kis/test_init.py b/tests/components/switcher_kis/test_init.py index aa187b80d8f..394f48d001a 100644 --- a/tests/components/switcher_kis/test_init.py +++ b/tests/components/switcher_kis/test_init.py @@ -1,21 +1,27 @@ """Test cases for the switcher_kis component.""" - from datetime import timedelta -from typing import TYPE_CHECKING, Any, Generator +from typing import Any, Generator +from unittest.mock import patch +from aioswitcher.consts import COMMAND_ON +from aioswitcher.devices import SwitcherV2Device from pytest import raises from homeassistant.components.switcher_kis import ( - CONF_AUTO_OFF, DATA_DEVICE, DOMAIN, - SERVICE_SET_AUTO_OFF_NAME, - SERVICE_SET_AUTO_OFF_SCHEMA, SIGNAL_SWITCHER_DEVICE_UPDATE, ) +from homeassistant.components.switcher_kis.switch import ( + CONF_AUTO_OFF, + CONF_TIMER_MINUTES, + SERVICE_SET_AUTO_OFF_NAME, + SERVICE_TURN_ON_WITH_TIMER_NAME, +) from homeassistant.const import CONF_ENTITY_ID from homeassistant.core import Context, callback -from homeassistant.exceptions import Unauthorized, UnknownUser +from homeassistant.exceptions import UnknownUser +from homeassistant.helpers.config_validation import time_period_str from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.typing import HomeAssistantType from homeassistant.setup import async_setup_component @@ -32,16 +38,12 @@ from .consts import ( DUMMY_PHONE_ID, DUMMY_POWER_CONSUMPTION, DUMMY_REMAINING_TIME, + DUMMY_TIMER_MINUTES_SET, MANDATORY_CONFIGURATION, SWITCH_ENTITY_ID, ) -from tests.common import async_fire_time_changed, async_mock_service - -if TYPE_CHECKING: - from aioswitcher.devices import SwitcherV2Device - - from tests.common import MockUser +from tests.common import MockUser, async_fire_time_changed async def test_failed_config( @@ -83,8 +85,7 @@ async def test_set_auto_off_service( hass: HomeAssistantType, mock_bridge: Generator[None, Any, None], mock_api: Generator[None, Any, None], - hass_owner_user: "MockUser", - hass_read_only_user: "MockUser", + hass_owner_user: MockUser, ) -> None: """Test the set_auto_off service.""" assert await async_setup_component(hass, DOMAIN, MANDATORY_CONFIGURATION) @@ -101,31 +102,6 @@ async def test_set_auto_off_service( context=Context(user_id=hass_owner_user.id), ) - with raises(Unauthorized) as unauthorized_read_only_exc: - await hass.services.async_call( - DOMAIN, - SERVICE_SET_AUTO_OFF_NAME, - {CONF_ENTITY_ID: SWITCH_ENTITY_ID, CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET}, - blocking=True, - context=Context(user_id=hass_read_only_user.id), - ) - - assert unauthorized_read_only_exc.type is Unauthorized - - with raises(Unauthorized) as unauthorized_wrong_entity_exc: - await hass.services.async_call( - DOMAIN, - SERVICE_SET_AUTO_OFF_NAME, - { - CONF_ENTITY_ID: "light.not_related_entity", - CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET, - }, - blocking=True, - context=Context(user_id=hass_owner_user.id), - ) - - assert unauthorized_wrong_entity_exc.type is Unauthorized - with raises(UnknownUser) as unknown_user_exc: await hass.services.async_call( DOMAIN, @@ -137,20 +113,74 @@ async def test_set_auto_off_service( assert unknown_user_exc.type is UnknownUser - service_calls = async_mock_service( - hass, DOMAIN, SERVICE_SET_AUTO_OFF_NAME, SERVICE_SET_AUTO_OFF_SCHEMA - ) + with patch( + "homeassistant.components.switcher_kis.switch.SwitcherV2Api.set_auto_shutdown" + ) as mock_set_auto_shutdown: + await hass.services.async_call( + DOMAIN, + SERVICE_SET_AUTO_OFF_NAME, + {CONF_ENTITY_ID: SWITCH_ENTITY_ID, CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET}, + ) - await hass.services.async_call( - DOMAIN, - SERVICE_SET_AUTO_OFF_NAME, - {CONF_ENTITY_ID: SWITCH_ENTITY_ID, CONF_AUTO_OFF: DUMMY_AUTO_OFF_SET}, - ) + await hass.async_block_till_done() + + mock_set_auto_shutdown.assert_called_once_with( + time_period_str(DUMMY_AUTO_OFF_SET) + ) + + +async def test_turn_on_with_timer_service( + hass: HomeAssistantType, + mock_bridge: Generator[None, Any, None], + mock_api: Generator[None, Any, None], + hass_owner_user: MockUser, +) -> None: + """Test the set_auto_off service.""" + assert await async_setup_component(hass, DOMAIN, MANDATORY_CONFIGURATION) await hass.async_block_till_done() - assert len(service_calls) == 1 - assert str(service_calls[0].data[CONF_AUTO_OFF]) == DUMMY_AUTO_OFF_SET.lstrip("0") + assert hass.services.has_service(DOMAIN, SERVICE_TURN_ON_WITH_TIMER_NAME) + + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON_WITH_TIMER_NAME, + {CONF_ENTITY_ID: SWITCH_ENTITY_ID, CONF_TIMER_MINUTES: DUMMY_TIMER_MINUTES_SET}, + blocking=True, + context=Context(user_id=hass_owner_user.id), + ) + + with raises(UnknownUser) as unknown_user_exc: + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON_WITH_TIMER_NAME, + { + CONF_ENTITY_ID: SWITCH_ENTITY_ID, + CONF_TIMER_MINUTES: DUMMY_TIMER_MINUTES_SET, + }, + blocking=True, + context=Context(user_id="not_real_user"), + ) + + assert unknown_user_exc.type is UnknownUser + + with patch( + "homeassistant.components.switcher_kis.switch.SwitcherV2Api.control_device" + ) as mock_control_device: + await hass.services.async_call( + DOMAIN, + SERVICE_TURN_ON_WITH_TIMER_NAME, + { + CONF_ENTITY_ID: SWITCH_ENTITY_ID, + CONF_TIMER_MINUTES: DUMMY_TIMER_MINUTES_SET, + }, + ) + + await hass.async_block_till_done() + + mock_control_device.assert_called_once_with( + COMMAND_ON, int(DUMMY_TIMER_MINUTES_SET) + ) async def test_signal_dispatcher( @@ -162,7 +192,7 @@ async def test_signal_dispatcher( await hass.async_block_till_done() @callback - def verify_update_data(device: "SwitcherV2Device") -> None: + def verify_update_data(device: SwitcherV2Device) -> None: """Use as callback for signal dispatcher.""" pass diff --git a/tests/components/syncthru/test_config_flow.py b/tests/components/syncthru/test_config_flow.py index 1df377ff0e8..70e8a80ad0f 100644 --- a/tests/components/syncthru/test_config_flow.py +++ b/tests/components/syncthru/test_config_flow.py @@ -1,6 +1,7 @@ """Tests for syncthru config flow.""" import re +from unittest.mock import patch from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components import ssdp @@ -8,7 +9,6 @@ from homeassistant.components.syncthru.config_flow import SyncThru from homeassistant.components.syncthru.const import DOMAIN from homeassistant.const import CONF_NAME, CONF_URL -from tests.async_mock import patch from tests.common import MockConfigEntry, mock_coro FIXTURE_USER_INPUT = { diff --git a/tests/components/synology_dsm/conftest.py b/tests/components/synology_dsm/conftest.py index db25bd59ada..74de072e229 100644 --- a/tests/components/synology_dsm/conftest.py +++ b/tests/components/synology_dsm/conftest.py @@ -1,7 +1,7 @@ """Configure Synology DSM tests.""" -import pytest +from unittest.mock import patch -from tests.async_mock import patch +import pytest def pytest_configure(config): diff --git a/tests/components/synology_dsm/test_config_flow.py b/tests/components/synology_dsm/test_config_flow.py index 59ed8eea657..85ed02a7a52 100644 --- a/tests/components/synology_dsm/test_config_flow.py +++ b/tests/components/synology_dsm/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for the Synology DSM config flow.""" +from unittest.mock import MagicMock, Mock, patch + import pytest from synology_dsm.exceptions import ( SynologyDSMException, @@ -50,7 +52,6 @@ from .consts import ( VERIFY_SSL, ) -from tests.async_mock import MagicMock, Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/synology_dsm/test_init.py b/tests/components/synology_dsm/test_init.py index b8be375b321..59864c56523 100644 --- a/tests/components/synology_dsm/test_init.py +++ b/tests/components/synology_dsm/test_init.py @@ -1,4 +1,6 @@ """Tests for the Synology DSM component.""" +from unittest.mock import patch + import pytest from homeassistant.components.synology_dsm.const import DOMAIN, SERVICES @@ -14,7 +16,6 @@ from homeassistant.helpers.typing import HomeAssistantType from .consts import HOST, MACS, PASSWORD, PORT, USE_SSL, USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/system_health/test_init.py b/tests/components/system_health/test_init.py index 28f54c81c42..212ec544629 100644 --- a/tests/components/system_health/test_init.py +++ b/tests/components/system_health/test_init.py @@ -1,12 +1,12 @@ """Tests for the system health component init.""" import asyncio +from unittest.mock import AsyncMock, Mock, patch from aiohttp.client_exceptions import ClientError from homeassistant.components import system_health from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, Mock, patch from tests.common import get_system_health_info, mock_platform diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py index 85e86d9c78f..287e7139aff 100644 --- a/tests/components/system_log/test_init.py +++ b/tests/components/system_log/test_init.py @@ -2,6 +2,7 @@ import asyncio import logging import queue +from unittest.mock import MagicMock, patch import pytest @@ -9,8 +10,6 @@ from homeassistant.bootstrap import async_setup_component from homeassistant.components import system_log from homeassistant.core import callback -from tests.async_mock import MagicMock, patch - _LOGGER = logging.getLogger("test_logger") BASIC_CONFIG = {"system_log": {"max_entries": 2}} diff --git a/tests/components/tado/test_binary_sensor.py b/tests/components/tado/test_binary_sensor.py index 39dd068f5a6..c811314e4f9 100644 --- a/tests/components/tado/test_binary_sensor.py +++ b/tests/components/tado/test_binary_sensor.py @@ -1,10 +1,64 @@ """The sensor tests for the tado platform.""" -from homeassistant.const import STATE_ON +from homeassistant.const import STATE_OFF, STATE_ON from .util import async_init_integration +async def test_air_con_create_binary_sensors(hass): + """Test creation of aircon sensors.""" + + await async_init_integration(hass) + + state = hass.states.get("binary_sensor.air_conditioning_power") + assert state.state == STATE_ON + + state = hass.states.get("binary_sensor.air_conditioning_link") + assert state.state == STATE_ON + + state = hass.states.get("binary_sensor.air_conditioning_overlay") + assert state.state == STATE_ON + + state = hass.states.get("binary_sensor.air_conditioning_open_window") + assert state.state == STATE_OFF + + +async def test_heater_create_binary_sensors(hass): + """Test creation of heater sensors.""" + + await async_init_integration(hass) + + state = hass.states.get("binary_sensor.baseboard_heater_power") + assert state.state == STATE_ON + + state = hass.states.get("binary_sensor.baseboard_heater_link") + assert state.state == STATE_ON + + state = hass.states.get("binary_sensor.baseboard_heater_early_start") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.baseboard_heater_overlay") + assert state.state == STATE_ON + + state = hass.states.get("binary_sensor.baseboard_heater_open_window") + assert state.state == STATE_OFF + + +async def test_water_heater_create_binary_sensors(hass): + """Test creation of water heater sensors.""" + + await async_init_integration(hass) + + state = hass.states.get("binary_sensor.water_heater_link") + assert state.state == STATE_ON + + state = hass.states.get("binary_sensor.water_heater_overlay") + assert state.state == STATE_OFF + + state = hass.states.get("binary_sensor.water_heater_power") + assert state.state == STATE_ON + + async def test_home_create_binary_sensors(hass): """Test creation of home binary sensors.""" diff --git a/tests/components/tado/test_config_flow.py b/tests/components/tado/test_config_flow.py index ce4af05b79c..c3e2bac68ca 100644 --- a/tests/components/tado/test_config_flow.py +++ b/tests/components/tado/test_config_flow.py @@ -1,11 +1,12 @@ """Test the Tado config flow.""" +from unittest.mock import MagicMock, patch + import requests from homeassistant import config_entries, setup from homeassistant.components.tado.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry @@ -54,38 +55,6 @@ async def test_form(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_import(hass): - """Test we can import.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - mock_tado_api = _get_mock_tado_api(getMe={"homes": [{"id": 1, "name": "myhome"}]}) - - with patch( - "homeassistant.components.tado.config_flow.Tado", - return_value=mock_tado_api, - ), patch( - "homeassistant.components.tado.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.tado.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={"username": "test-username", "password": "test-password"}, - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == "myhome" - assert result["data"] == { - "username": "test-username", - "password": "test-password", - } - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - async def test_form_invalid_auth(hass): """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/tado/test_sensor.py b/tests/components/tado/test_sensor.py index 646e7741530..2fac88bc22e 100644 --- a/tests/components/tado/test_sensor.py +++ b/tests/components/tado/test_sensor.py @@ -8,15 +8,6 @@ async def test_air_con_create_sensors(hass): await async_init_integration(hass) - state = hass.states.get("sensor.air_conditioning_power") - assert state.state == "ON" - - state = hass.states.get("sensor.air_conditioning_link") - assert state.state == "ONLINE" - - state = hass.states.get("sensor.air_conditioning_link") - assert state.state == "ONLINE" - state = hass.states.get("sensor.air_conditioning_tado_mode") assert state.state == "HOME" @@ -26,48 +17,24 @@ async def test_air_con_create_sensors(hass): state = hass.states.get("sensor.air_conditioning_ac") assert state.state == "ON" - state = hass.states.get("sensor.air_conditioning_overlay") - assert state.state == "True" - state = hass.states.get("sensor.air_conditioning_humidity") assert state.state == "60.9" - state = hass.states.get("sensor.air_conditioning_open_window") - assert state.state == "False" - async def test_heater_create_sensors(hass): """Test creation of heater sensors.""" await async_init_integration(hass) - state = hass.states.get("sensor.baseboard_heater_power") - assert state.state == "ON" - - state = hass.states.get("sensor.baseboard_heater_link") - assert state.state == "ONLINE" - - state = hass.states.get("sensor.baseboard_heater_link") - assert state.state == "ONLINE" - state = hass.states.get("sensor.baseboard_heater_tado_mode") assert state.state == "HOME" state = hass.states.get("sensor.baseboard_heater_temperature") assert state.state == "20.65" - state = hass.states.get("sensor.baseboard_heater_early_start") - assert state.state == "False" - - state = hass.states.get("sensor.baseboard_heater_overlay") - assert state.state == "True" - state = hass.states.get("sensor.baseboard_heater_humidity") assert state.state == "45.2" - state = hass.states.get("sensor.baseboard_heater_open_window") - assert state.state == "False" - async def test_water_heater_create_sensors(hass): """Test creation of water heater sensors.""" @@ -76,12 +43,3 @@ async def test_water_heater_create_sensors(hass): state = hass.states.get("sensor.water_heater_tado_mode") assert state.state == "HOME" - - state = hass.states.get("sensor.water_heater_link") - assert state.state == "ONLINE" - - state = hass.states.get("sensor.water_heater_overlay") - assert state.state == "False" - - state = hass.states.get("sensor.water_heater_power") - assert state.state == "ON" diff --git a/tests/components/tado/util.py b/tests/components/tado/util.py index 7dc6a21faa7..d27ede47a63 100644 --- a/tests/components/tado/util.py +++ b/tests/components/tado/util.py @@ -20,6 +20,9 @@ async def async_init_integration( me_fixture = "tado/me.json" zones_fixture = "tado/zones.json" + # WR1 Device + device_wr1_fixture = "tado/device_wr1.json" + # Smart AC with Swing zone_5_state_fixture = "tado/smartac3.with_swing.json" zone_5_capabilities_fixture = "tado/zone_with_swing_capabilities.json" @@ -40,6 +43,9 @@ async def async_init_integration( zone_1_state_fixture = "tado/tadov2.heating.manual_mode.json" zone_1_capabilities_fixture = "tado/tadov2.zone_capabilities.json" + # Device Temp Offset + device_temp_offset = "tado/device_temp_offset.json" + with requests_mock.mock() as m: m.post("https://auth.tado.com/oauth/token", text=load_fixture(token_fixture)) m.get( @@ -50,6 +56,18 @@ async def async_init_integration( "https://my.tado.com/api/v2/homes/1/devices", text=load_fixture(devices_fixture), ) + m.get( + "https://my.tado.com/api/v2/devices/WR1/", + text=load_fixture(device_wr1_fixture), + ) + m.get( + "https://my.tado.com/api/v2/devices/WR1/temperatureOffset", + text=load_fixture(device_temp_offset), + ) + m.get( + "https://my.tado.com/api/v2/devices/WR4/temperatureOffset", + text=load_fixture(device_temp_offset), + ) m.get( "https://my.tado.com/api/v2/homes/1/zones", text=load_fixture(zones_fixture), diff --git a/tests/components/tag/test_init.py b/tests/components/tag/test_init.py index 442d3263f0a..2f9ff2310cf 100644 --- a/tests/components/tag/test_init.py +++ b/tests/components/tag/test_init.py @@ -1,4 +1,6 @@ """Tests for the tag component.""" +from unittest.mock import patch + import pytest from homeassistant.components.tag import DOMAIN, TAGS, async_scan_tag @@ -6,8 +8,6 @@ from homeassistant.helpers import collection from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util -from tests.async_mock import patch - @pytest.fixture def storage_setup(hass, hass_storage): diff --git a/tests/components/tasmota/conftest.py b/tests/components/tasmota/conftest.py index fadf8a44648..3c530a93d1e 100644 --- a/tests/components/tasmota/conftest.py +++ b/tests/components/tasmota/conftest.py @@ -1,4 +1,6 @@ """Test fixtures for Tasmota component.""" +from unittest.mock import patch + from hatasmota.discovery import get_status_sensor_entities import pytest @@ -9,7 +11,6 @@ from homeassistant.components.tasmota.const import ( DOMAIN, ) -from tests.async_mock import patch from tests.common import ( MockConfigEntry, async_mock_service, diff --git a/tests/components/tasmota/test_binary_sensor.py b/tests/components/tasmota/test_binary_sensor.py index 3f444e75bdc..6d4263853dc 100644 --- a/tests/components/tasmota/test_binary_sensor.py +++ b/tests/components/tasmota/test_binary_sensor.py @@ -2,6 +2,7 @@ import copy from datetime import timedelta import json +from unittest.mock import patch from hatasmota.utils import ( get_topic_stat_result, @@ -34,7 +35,6 @@ from .test_common import ( help_test_entity_id_update_subscriptions, ) -from tests.async_mock import patch from tests.common import async_fire_mqtt_message, async_fire_time_changed diff --git a/tests/components/tasmota/test_common.py b/tests/components/tasmota/test_common.py index 04346f915c4..973ecd3c890 100644 --- a/tests/components/tasmota/test_common.py +++ b/tests/components/tasmota/test_common.py @@ -1,6 +1,7 @@ """Common test objects.""" import copy import json +from unittest.mock import ANY from hatasmota.const import ( CONF_MAC, @@ -20,7 +21,6 @@ from hatasmota.utils import ( from homeassistant.components.tasmota.const import DEFAULT_PREFIX from homeassistant.const import STATE_UNAVAILABLE -from tests.async_mock import ANY from tests.common import async_fire_mqtt_message DEFAULT_CONFIG = { diff --git a/tests/components/tasmota/test_cover.py b/tests/components/tasmota/test_cover.py index d5ae01f1666..131f95842a5 100644 --- a/tests/components/tasmota/test_cover.py +++ b/tests/components/tasmota/test_cover.py @@ -1,6 +1,7 @@ """The tests for the Tasmota cover platform.""" import copy import json +from unittest.mock import patch from hatasmota.utils import ( get_topic_stat_result, @@ -26,7 +27,6 @@ from .test_common import ( help_test_entity_id_update_subscriptions, ) -from tests.async_mock import patch from tests.common import async_fire_mqtt_message diff --git a/tests/components/tasmota/test_device_trigger.py b/tests/components/tasmota/test_device_trigger.py index 2c88533f30d..ec8744881c5 100644 --- a/tests/components/tasmota/test_device_trigger.py +++ b/tests/components/tasmota/test_device_trigger.py @@ -1,6 +1,7 @@ """The tests for MQTT device triggers.""" import copy import json +from unittest.mock import patch from hatasmota.switch import TasmotaSwitchTriggerConfig import pytest @@ -12,7 +13,6 @@ from homeassistant.setup import async_setup_component from .test_common import DEFAULT_CONFIG -from tests.async_mock import patch from tests.common import ( assert_lists_same, async_fire_mqtt_message, diff --git a/tests/components/tasmota/test_discovery.py b/tests/components/tasmota/test_discovery.py index 40fecb6b695..35be7e50e62 100644 --- a/tests/components/tasmota/test_discovery.py +++ b/tests/components/tasmota/test_discovery.py @@ -1,6 +1,7 @@ """The tests for the MQTT discovery.""" import copy import json +from unittest.mock import patch from homeassistant.components.tasmota.const import DEFAULT_PREFIX from homeassistant.components.tasmota.discovery import ALREADY_DISCOVERED @@ -8,7 +9,6 @@ from homeassistant.components.tasmota.discovery import ALREADY_DISCOVERED from .conftest import setup_tasmota_helper from .test_common import DEFAULT_CONFIG, DEFAULT_CONFIG_9_0_0_3 -from tests.async_mock import patch from tests.common import async_fire_mqtt_message diff --git a/tests/components/tasmota/test_fan.py b/tests/components/tasmota/test_fan.py index 1aca8c84e07..4035c877bb8 100644 --- a/tests/components/tasmota/test_fan.py +++ b/tests/components/tasmota/test_fan.py @@ -1,6 +1,7 @@ """The tests for the Tasmota fan platform.""" import copy import json +from unittest.mock import patch from hatasmota.utils import ( get_topic_stat_result, @@ -26,7 +27,6 @@ from .test_common import ( help_test_entity_id_update_subscriptions, ) -from tests.async_mock import patch from tests.common import async_fire_mqtt_message from tests.components.fan import common diff --git a/tests/components/tasmota/test_init.py b/tests/components/tasmota/test_init.py index 39d99872741..5b553164583 100644 --- a/tests/components/tasmota/test_init.py +++ b/tests/components/tasmota/test_init.py @@ -1,13 +1,13 @@ """The tests for the Tasmota binary sensor platform.""" import copy import json +from unittest.mock import call from homeassistant.components import websocket_api from homeassistant.components.tasmota.const import DEFAULT_PREFIX from .test_common import DEFAULT_CONFIG -from tests.async_mock import call from tests.common import MockConfigEntry, async_fire_mqtt_message diff --git a/tests/components/tasmota/test_light.py b/tests/components/tasmota/test_light.py index f09c27da753..d64e39aacf0 100644 --- a/tests/components/tasmota/test_light.py +++ b/tests/components/tasmota/test_light.py @@ -1,6 +1,7 @@ """The tests for the Tasmota light platform.""" import copy import json +from unittest.mock import patch from hatasmota.const import CONF_MAC from hatasmota.utils import ( @@ -34,7 +35,6 @@ from .test_common import ( help_test_entity_id_update_subscriptions, ) -from tests.async_mock import patch from tests.common import async_fire_mqtt_message from tests.components.light import common @@ -789,7 +789,7 @@ async def test_sending_mqtt_commands_rgbww(hass, mqtt_mock, setup_tasmota): # Turn the light on and verify MQTT message is sent await common.async_turn_on(hass, "light.test") mqtt_mock.async_publish.assert_called_once_with( - "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade 0;NoDelay;Power1 ON", 0, False + "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON", 0, False ) mqtt_mock.async_publish.reset_mock() @@ -800,21 +800,21 @@ async def test_sending_mqtt_commands_rgbww(hass, mqtt_mock, setup_tasmota): # Turn the light off and verify MQTT message is sent await common.async_turn_off(hass, "light.test") mqtt_mock.async_publish.assert_called_once_with( - "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade 0;NoDelay;Power1 OFF", 0, False + "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 OFF", 0, False ) mqtt_mock.async_publish.reset_mock() # Turn the light on and verify MQTT messages are sent await common.async_turn_on(hass, "light.test", brightness=192) mqtt_mock.async_publish.assert_called_once_with( - "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade 0;NoDelay;Dimmer 75", 0, False + "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Dimmer 75", 0, False ) mqtt_mock.async_publish.reset_mock() await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Fade 0;NoDelay;Power1 ON;NoDelay;Color2 255,128,0", + "NoDelay;Power1 ON;NoDelay;Color2 255,128,0", 0, False, ) @@ -823,7 +823,7 @@ async def test_sending_mqtt_commands_rgbww(hass, mqtt_mock, setup_tasmota): await common.async_turn_on(hass, "light.test", color_temp=200) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Fade 0;NoDelay;Power1 ON;NoDelay;CT 200", + "NoDelay;Power1 ON;NoDelay;CT 200", 0, False, ) @@ -832,7 +832,7 @@ async def test_sending_mqtt_commands_rgbww(hass, mqtt_mock, setup_tasmota): await common.async_turn_on(hass, "light.test", white_value=128) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Fade 0;NoDelay;Power1 ON;NoDelay;White 50", + "NoDelay;Power1 ON;NoDelay;White 50", 0, False, ) @@ -841,7 +841,7 @@ async def test_sending_mqtt_commands_rgbww(hass, mqtt_mock, setup_tasmota): await common.async_turn_on(hass, "light.test", effect="Random") mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Fade 0;NoDelay;Power1 ON;NoDelay;Scheme 4", + "NoDelay;Power1 ON;NoDelay;Scheme 4", 0, False, ) @@ -873,7 +873,7 @@ async def test_sending_mqtt_commands_power_unlinked(hass, mqtt_mock, setup_tasmo # Turn the light on and verify MQTT message is sent await common.async_turn_on(hass, "light.test") mqtt_mock.async_publish.assert_called_once_with( - "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade 0;NoDelay;Power1 ON", 0, False + "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 ON", 0, False ) mqtt_mock.async_publish.reset_mock() @@ -884,7 +884,7 @@ async def test_sending_mqtt_commands_power_unlinked(hass, mqtt_mock, setup_tasmo # Turn the light off and verify MQTT message is sent await common.async_turn_off(hass, "light.test") mqtt_mock.async_publish.assert_called_once_with( - "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Fade 0;NoDelay;Power1 OFF", 0, False + "tasmota_49A3BC/cmnd/Backlog", "NoDelay;Power1 OFF", 0, False ) mqtt_mock.async_publish.reset_mock() @@ -892,7 +892,7 @@ async def test_sending_mqtt_commands_power_unlinked(hass, mqtt_mock, setup_tasmo await common.async_turn_on(hass, "light.test", brightness=192) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - "NoDelay;Fade 0;NoDelay;Dimmer 75;NoDelay;Power1 ON", + "NoDelay;Dimmer 75;NoDelay;Power1 ON", 0, False, ) @@ -978,6 +978,24 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): ) mqtt_mock.async_publish.reset_mock() + # Fake state update from the light + async_fire_mqtt_message( + hass, "tasmota_49A3BC/tele/STATE", '{"POWER":"ON","Dimmer":100}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 255 + + # Dim the light from 100->0: Speed should be 0 + await common.async_turn_off(hass, "light.test", transition=0) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Fade 0;NoDelay;Power1 OFF", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + # Fake state update from the light async_fire_mqtt_message( hass, @@ -1059,6 +1077,79 @@ async def test_transition(hass, mqtt_mock, setup_tasmota): mqtt_mock.async_publish.reset_mock() +async def test_transition_fixed(hass, mqtt_mock, setup_tasmota): + """Test transition commands.""" + config = copy.deepcopy(DEFAULT_CONFIG) + config["rl"][0] = 2 + config["lt_st"] = 5 # 5 channel light (RGBCW) + config["so"]["117"] = 1 # fading at fixed duration instead of fixed slew rate + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + state = hass.states.get("light.test") + assert state.state == STATE_OFF + await hass.async_block_till_done() + await hass.async_block_till_done() + mqtt_mock.async_publish.reset_mock() + + # Dim the light from 0->100: Speed should be 4*2=8 + await common.async_turn_on(hass, "light.test", brightness=255, transition=4) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Fade 1;NoDelay;Speed 8;NoDelay;Dimmer 100", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + # Dim the light from 0->100: Speed should be capped at 40 + await common.async_turn_on(hass, "light.test", brightness=255, transition=100) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Fade 1;NoDelay;Speed 40;NoDelay;Dimmer 100", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + # Dim the light from 0->0: Speed should be 4*2=8 + await common.async_turn_on(hass, "light.test", brightness=0, transition=4) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Fade 1;NoDelay;Speed 8;NoDelay;Power1 OFF", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + # Dim the light from 0->50: Speed should be 4*2=8 + await common.async_turn_on(hass, "light.test", brightness=128, transition=4) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Fade 1;NoDelay;Speed 8;NoDelay;Dimmer 50", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + # Dim the light from 0->50: Speed should be 0 + await common.async_turn_on(hass, "light.test", brightness=128, transition=0) + mqtt_mock.async_publish.assert_called_once_with( + "tasmota_49A3BC/cmnd/Backlog", + "NoDelay;Fade 0;NoDelay;Dimmer 50", + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + + async def test_relay_as_light(hass, mqtt_mock, setup_tasmota): """Test relay show up as light in light mode.""" config = copy.deepcopy(DEFAULT_CONFIG) @@ -1104,7 +1195,7 @@ async def _test_split_light(hass, mqtt_mock, config, num_lights, num_switches): await common.async_turn_on(hass, entity) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - f"NoDelay;Fade 0;NoDelay;Power{idx+num_switches+1} ON", + f"NoDelay;Power{idx+num_switches+1} ON", 0, False, ) @@ -1114,7 +1205,7 @@ async def _test_split_light(hass, mqtt_mock, config, num_lights, num_switches): await common.async_turn_on(hass, entity, brightness=(idx + 1) * 25.5) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - f"NoDelay;Fade 0;NoDelay;Channel{idx+num_switches+1} {(idx+1)*10}", + f"NoDelay;Channel{idx+num_switches+1} {(idx+1)*10}", 0, False, ) @@ -1176,7 +1267,7 @@ async def _test_unlinked_light(hass, mqtt_mock, config, num_switches): await common.async_turn_on(hass, entity) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - f"NoDelay;Fade 0;NoDelay;Power{idx+num_switches+1} ON", + f"NoDelay;Power{idx+num_switches+1} ON", 0, False, ) @@ -1186,7 +1277,7 @@ async def _test_unlinked_light(hass, mqtt_mock, config, num_switches): await common.async_turn_on(hass, entity, brightness=(idx + 1) * 25.5) mqtt_mock.async_publish.assert_called_once_with( "tasmota_49A3BC/cmnd/Backlog", - f"NoDelay;Fade 0;NoDelay;Dimmer{idx+1} {(idx+1)*10}", + f"NoDelay;Dimmer{idx+1} {(idx+1)*10}", 0, False, ) diff --git a/tests/components/tasmota/test_mixins.py b/tests/components/tasmota/test_mixins.py index 5f4ef443475..579f8e9aaeb 100644 --- a/tests/components/tasmota/test_mixins.py +++ b/tests/components/tasmota/test_mixins.py @@ -1,6 +1,7 @@ """The tests for the Tasmota mixins.""" import copy import json +from unittest.mock import call from hatasmota.const import CONF_MAC from hatasmota.utils import config_get_state_online, get_topic_tele_will @@ -9,7 +10,6 @@ from homeassistant.components.tasmota.const import DEFAULT_PREFIX from .test_common import DEFAULT_CONFIG -from tests.async_mock import call from tests.common import async_fire_mqtt_message diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index 4c8de9e339d..fe415c264ef 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -3,6 +3,7 @@ import copy import datetime from datetime import timedelta import json +from unittest.mock import Mock, patch import hatasmota from hatasmota.utils import ( @@ -31,7 +32,6 @@ from .test_common import ( help_test_entity_id_update_subscriptions, ) -from tests.async_mock import Mock, patch from tests.common import async_fire_mqtt_message, async_fire_time_changed DEFAULT_SENSOR_CONFIG = { diff --git a/tests/components/tasmota/test_switch.py b/tests/components/tasmota/test_switch.py index 208026c2de5..00b0a922e0a 100644 --- a/tests/components/tasmota/test_switch.py +++ b/tests/components/tasmota/test_switch.py @@ -1,6 +1,7 @@ """The tests for the Tasmota switch platform.""" import copy import json +from unittest.mock import patch from hatasmota.utils import ( get_topic_stat_result, @@ -25,7 +26,6 @@ from .test_common import ( help_test_entity_id_update_subscriptions, ) -from tests.async_mock import patch from tests.common import async_fire_mqtt_message from tests.components.switch import common diff --git a/tests/components/tcp/test_binary_sensor.py b/tests/components/tcp/test_binary_sensor.py index 4cde4d9ac31..2dc16ad79c7 100644 --- a/tests/components/tcp/test_binary_sensor.py +++ b/tests/components/tcp/test_binary_sensor.py @@ -1,11 +1,11 @@ """The tests for the TCP binary sensor platform.""" import unittest +from unittest.mock import Mock, patch from homeassistant.components.tcp import binary_sensor as bin_tcp import homeassistant.components.tcp.sensor as tcp from homeassistant.setup import setup_component -from tests.async_mock import Mock, patch from tests.common import assert_setup_component, get_test_home_assistant import tests.components.tcp.test_sensor as test_tcp diff --git a/tests/components/tcp/test_sensor.py b/tests/components/tcp/test_sensor.py index b06652dc53f..8e79d4e514d 100644 --- a/tests/components/tcp/test_sensor.py +++ b/tests/components/tcp/test_sensor.py @@ -2,6 +2,7 @@ from copy import copy import socket import unittest +from unittest.mock import Mock, patch from uuid import uuid4 import homeassistant.components.tcp.sensor as tcp @@ -9,7 +10,6 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.template import Template from homeassistant.setup import setup_component -from tests.async_mock import Mock, patch from tests.common import assert_setup_component, get_test_home_assistant TEST_CONFIG = { diff --git a/tests/components/telegram/test_notify.py b/tests/components/telegram/test_notify.py index 7488db49d9e..6f8d1f989f9 100644 --- a/tests/components/telegram/test_notify.py +++ b/tests/components/telegram/test_notify.py @@ -1,5 +1,6 @@ """The tests for the telegram.notify platform.""" from os import path +from unittest.mock import patch from homeassistant import config as hass_config import homeassistant.components.notify as notify @@ -7,8 +8,6 @@ from homeassistant.components.telegram import DOMAIN from homeassistant.const import SERVICE_RELOAD from homeassistant.setup import async_setup_component -from tests.async_mock import patch - async def test_reload_notify(hass): """Verify we can reload the notify service.""" diff --git a/tests/components/template/test_binary_sensor.py b/tests/components/template/test_binary_sensor.py index e8ff4c83f8d..241ac88328e 100644 --- a/tests/components/template/test_binary_sensor.py +++ b/tests/components/template/test_binary_sensor.py @@ -1,6 +1,7 @@ """The tests for the Template Binary sensor platform.""" from datetime import timedelta import logging +from unittest.mock import patch from homeassistant import setup from homeassistant.components import binary_sensor @@ -14,7 +15,6 @@ from homeassistant.const import ( from homeassistant.core import CoreState import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import assert_setup_component, async_fire_time_changed diff --git a/tests/components/template/test_sensor.py b/tests/components/template/test_sensor.py index c39fb474bca..7f560fa0abb 100644 --- a/tests/components/template/test_sensor.py +++ b/tests/components/template/test_sensor.py @@ -1,6 +1,7 @@ """The test for the Template sensor platform.""" from asyncio import Event from datetime import timedelta +from unittest.mock import patch from homeassistant.bootstrap import async_from_config_dict from homeassistant.components import sensor @@ -18,7 +19,6 @@ from homeassistant.helpers.template import Template from homeassistant.setup import ATTR_COMPONENT, async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import assert_setup_component, async_fire_time_changed diff --git a/tests/components/template/test_trigger.py b/tests/components/template/test_trigger.py index 822a274bf23..de4974cb1b6 100644 --- a/tests/components/template/test_trigger.py +++ b/tests/components/template/test_trigger.py @@ -1,6 +1,7 @@ """The tests for the Template automation.""" from datetime import timedelta from unittest import mock +from unittest.mock import patch import pytest @@ -11,7 +12,6 @@ from homeassistant.core import Context, callback from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import ( assert_setup_component, async_fire_time_changed, diff --git a/tests/components/tesla/test_config_flow.py b/tests/components/tesla/test_config_flow.py index 59cdf910bf4..7fb308ecc43 100644 --- a/tests/components/tesla/test_config_flow.py +++ b/tests/components/tesla/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Tesla config flow.""" +from unittest.mock import patch + from teslajsonpy import TeslaException from homeassistant import config_entries, data_entry_flow, setup @@ -18,7 +20,6 @@ from homeassistant.const import ( HTTP_NOT_FOUND, ) -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/tibber/test_config_flow.py b/tests/components/tibber/test_config_flow.py index c92ec5bc5c7..479f314123a 100644 --- a/tests/components/tibber/test_config_flow.py +++ b/tests/components/tibber/test_config_flow.py @@ -1,10 +1,11 @@ """Tests for Tibber config flow.""" +from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch + import pytest from homeassistant.components.tibber.const import DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN -from tests.async_mock import AsyncMock, MagicMock, PropertyMock, patch from tests.common import MockConfigEntry diff --git a/tests/components/tile/test_config_flow.py b/tests/components/tile/test_config_flow.py index b9d4799dc2f..e5561133a35 100644 --- a/tests/components/tile/test_config_flow.py +++ b/tests/components/tile/test_config_flow.py @@ -1,4 +1,6 @@ """Define tests for the Tile config flow.""" +from unittest.mock import patch + from pytile.errors import TileError from homeassistant import data_entry_flow @@ -6,7 +8,6 @@ from homeassistant.components.tile import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/time_date/test_sensor.py b/tests/components/time_date/test_sensor.py index b502dad3ea6..a9b5ea83c05 100644 --- a/tests/components/time_date/test_sensor.py +++ b/tests/components/time_date/test_sensor.py @@ -1,11 +1,11 @@ """The tests for time_date sensor platform.""" +from unittest.mock import patch + import pytest import homeassistant.components.time_date.sensor as time_date import homeassistant.util.dt as dt_util -from tests.async_mock import patch - ORIG_TZ = dt_util.DEFAULT_TIME_ZONE diff --git a/tests/components/timer/test_init.py b/tests/components/timer/test_init.py index 6b5a2596b71..74c3eceeea2 100644 --- a/tests/components/timer/test_init.py +++ b/tests/components/timer/test_init.py @@ -2,6 +2,7 @@ # pylint: disable=protected-access from datetime import timedelta import logging +from unittest.mock import patch import pytest @@ -42,7 +43,6 @@ from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from tests.async_mock import patch from tests.common import async_fire_time_changed _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/tod/test_binary_sensor.py b/tests/components/tod/test_binary_sensor.py index b0bca3e1c01..dff1e208ab6 100644 --- a/tests/components/tod/test_binary_sensor.py +++ b/tests/components/tod/test_binary_sensor.py @@ -1,5 +1,6 @@ """Test Times of the Day Binary Sensor.""" from datetime import datetime, timedelta +from unittest.mock import patch import pytest import pytz @@ -10,7 +11,6 @@ from homeassistant.helpers.sun import get_astral_event_date, get_astral_event_ne from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import assert_setup_component diff --git a/tests/components/toon/test_config_flow.py b/tests/components/toon/test_config_flow.py index b7eb3898b47..f3240991a37 100644 --- a/tests/components/toon/test_config_flow.py +++ b/tests/components/toon/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for the Toon config flow.""" +from unittest.mock import patch + from toonapi import Agreement, ToonError from homeassistant import data_entry_flow @@ -9,7 +11,6 @@ from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/totalconnect/common.py b/tests/components/totalconnect/common.py index c2d6f92015c..17fa244f9b2 100644 --- a/tests/components/totalconnect/common.py +++ b/tests/components/totalconnect/common.py @@ -1,11 +1,12 @@ """Common methods used across tests for TotalConnect.""" +from unittest.mock import patch + from total_connect_client import TotalConnectClient from homeassistant.components.totalconnect import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry LOCATION_INFO_BASIC_NORMAL = { diff --git a/tests/components/totalconnect/test_alarm_control_panel.py b/tests/components/totalconnect/test_alarm_control_panel.py index 75e07f09bf7..bc90c1aae2a 100644 --- a/tests/components/totalconnect/test_alarm_control_panel.py +++ b/tests/components/totalconnect/test_alarm_control_panel.py @@ -1,4 +1,6 @@ """Tests for the TotalConnect alarm control panel device.""" +from unittest.mock import patch + import pytest from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN @@ -24,8 +26,6 @@ from .common import ( setup_platform, ) -from tests.async_mock import patch - ENTITY_ID = "alarm_control_panel.test" CODE = "-1" DATA = {ATTR_ENTITY_ID: ENTITY_ID} diff --git a/tests/components/totalconnect/test_config_flow.py b/tests/components/totalconnect/test_config_flow.py index fd8cdcc5116..a1aa8780cfb 100644 --- a/tests/components/totalconnect/test_config_flow.py +++ b/tests/components/totalconnect/test_config_flow.py @@ -1,10 +1,11 @@ """Tests for the iCloud config flow.""" +from unittest.mock import patch + from homeassistant import data_entry_flow from homeassistant.components.totalconnect.const import DOMAIN from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry USERNAME = "username@me.com" diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index 48812f8fb2b..4c34e754ec3 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -2,6 +2,7 @@ from datetime import timedelta import logging from typing import Callable, NamedTuple +from unittest.mock import Mock, PropertyMock, patch from pyHS100 import SmartDeviceException import pytest @@ -33,7 +34,6 @@ from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow -from tests.async_mock import Mock, PropertyMock, patch from tests.common import async_fire_time_changed diff --git a/tests/components/traccar/test_init.py b/tests/components/traccar/test_init.py index 12e1ff5562a..c7e031b2ca6 100644 --- a/tests/components/traccar/test_init.py +++ b/tests/components/traccar/test_init.py @@ -1,4 +1,6 @@ """The tests the for Traccar device tracker platform.""" +from unittest.mock import patch + import pytest from homeassistant import data_entry_flow @@ -15,8 +17,6 @@ from homeassistant.const import ( from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component -from tests.async_mock import patch - HOME_LATITUDE = 37.239622 HOME_LONGITUDE = -115.815811 diff --git a/tests/components/tradfri/conftest.py b/tests/components/tradfri/conftest.py index c95bf2c036c..93675a9e4d1 100644 --- a/tests/components/tradfri/conftest.py +++ b/tests/components/tradfri/conftest.py @@ -1,9 +1,10 @@ """Common tradfri test fixtures.""" +from unittest.mock import Mock, patch + import pytest from . import MOCK_GATEWAY_ID -from tests.async_mock import Mock, patch from tests.components.light.conftest import mock_light_profiles # noqa # pylint: disable=protected-access diff --git a/tests/components/tradfri/test_config_flow.py b/tests/components/tradfri/test_config_flow.py index c9f72b7a9df..a155e8b383c 100644 --- a/tests/components/tradfri/test_config_flow.py +++ b/tests/components/tradfri/test_config_flow.py @@ -1,10 +1,11 @@ """Test the Tradfri config flow.""" +from unittest.mock import patch + import pytest from homeassistant import data_entry_flow from homeassistant.components.tradfri import config_flow -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/tradfri/test_init.py b/tests/components/tradfri/test_init.py index 34cc6d38091..0983b5aa22f 100644 --- a/tests/components/tradfri/test_init.py +++ b/tests/components/tradfri/test_init.py @@ -1,4 +1,6 @@ """Tests for Tradfri setup.""" +from unittest.mock import patch + from homeassistant.components import tradfri from homeassistant.helpers.device_registry import ( async_entries_for_config_entry, @@ -6,7 +8,6 @@ from homeassistant.helpers.device_registry import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/tradfri/test_light.py b/tests/components/tradfri/test_light.py index cf11d42411e..a4a1006d7fe 100644 --- a/tests/components/tradfri/test_light.py +++ b/tests/components/tradfri/test_light.py @@ -1,6 +1,7 @@ """Tradfri lights platform tests.""" from copy import deepcopy +from unittest.mock import MagicMock, Mock, PropertyMock, patch import pytest from pytradfri.device import Device @@ -11,7 +12,6 @@ from homeassistant.components import tradfri from . import MOCK_GATEWAY_ID -from tests.async_mock import MagicMock, Mock, PropertyMock, patch from tests.common import MockConfigEntry DEFAULT_TEST_FEATURES = { diff --git a/tests/components/transmission/test_config_flow.py b/tests/components/transmission/test_config_flow.py index 0b57ab59913..2982d363da9 100644 --- a/tests/components/transmission/test_config_flow.py +++ b/tests/components/transmission/test_config_flow.py @@ -1,5 +1,6 @@ """Tests for Transmission config flow.""" from datetime import timedelta +from unittest.mock import patch import pytest from transmissionrpc.error import TransmissionError @@ -25,7 +26,6 @@ from homeassistant.const import ( CONF_USERNAME, ) -from tests.async_mock import patch from tests.common import MockConfigEntry NAME = "Transmission" diff --git a/tests/components/transport_nsw/test_sensor.py b/tests/components/transport_nsw/test_sensor.py index a6f9c7dfd7f..baf7f793426 100644 --- a/tests/components/transport_nsw/test_sensor.py +++ b/tests/components/transport_nsw/test_sensor.py @@ -1,7 +1,7 @@ """The tests for the Transport NSW (AU) sensor platform.""" -from homeassistant.setup import async_setup_component +from unittest.mock import patch -from tests.async_mock import patch +from homeassistant.setup import async_setup_component VALID_CONFIG = { "sensor": { diff --git a/tests/components/trend/test_binary_sensor.py b/tests/components/trend/test_binary_sensor.py index 43db97b8f80..d1d77001bff 100644 --- a/tests/components/trend/test_binary_sensor.py +++ b/tests/components/trend/test_binary_sensor.py @@ -1,13 +1,13 @@ """The test for the Trend sensor platform.""" from datetime import timedelta from os import path +from unittest.mock import patch from homeassistant import config as hass_config, setup from homeassistant.components.trend import DOMAIN from homeassistant.const import SERVICE_RELOAD import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index d66efec8c64..77fbd3f7170 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -1,4 +1,6 @@ """The tests for the TTS component.""" +from unittest.mock import PropertyMock, patch + import pytest import yarl @@ -16,7 +18,6 @@ from homeassistant.config import async_process_ha_core_config from homeassistant.const import HTTP_NOT_FOUND from homeassistant.setup import async_setup_component -from tests.async_mock import PropertyMock, patch from tests.common import assert_setup_component, async_mock_service @@ -698,9 +699,10 @@ async def test_setup_component_and_web_get_url(hass, hass_client): req = await client.post(url, json=data) assert req.status == 200 response = await req.json() - assert response.get("url") == ( - "http://example.local:8123/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" - ) + assert response == { + "url": "http://example.local:8123/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", + "path": "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", + } async def test_setup_component_and_web_get_url_bad_config(hass, hass_client): diff --git a/tests/components/tts/test_notify.py b/tests/components/tts/test_notify.py index 90c0175831f..9989b1d349e 100644 --- a/tests/components/tts/test_notify.py +++ b/tests/components/tts/test_notify.py @@ -1,4 +1,6 @@ """The tests for the TTS component.""" +from unittest.mock import patch + import pytest import yarl @@ -12,7 +14,6 @@ import homeassistant.components.tts as tts from homeassistant.config import async_process_ha_core_config from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import assert_setup_component, async_mock_service diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index e1b9bd3466c..0055b451e1a 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for the Tuya config flow.""" +from unittest.mock import Mock, patch + import pytest from tuyaha.tuyaapi import TuyaAPIException, TuyaNetException @@ -6,7 +8,6 @@ from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.tuya.const import CONF_COUNTRYCODE, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry USERNAME = "myUsername" diff --git a/tests/components/twilio/test_init.py b/tests/components/twilio/test_init.py index 185139077df..ee7f072a65c 100644 --- a/tests/components/twilio/test_init.py +++ b/tests/components/twilio/test_init.py @@ -1,10 +1,10 @@ """Test the init file of Twilio.""" +from unittest.mock import patch + from homeassistant import data_entry_flow from homeassistant.components import twilio from homeassistant.core import callback -from tests.async_mock import patch - async def test_config_flow_registers_webhook(hass, aiohttp_client): """Test setting up Twilio and sending webhook.""" diff --git a/tests/components/twinkly/test_config_flow.py b/tests/components/twinkly/test_config_flow.py index d1a56277fa7..afbe608219f 100644 --- a/tests/components/twinkly/test_config_flow.py +++ b/tests/components/twinkly/test_config_flow.py @@ -1,5 +1,7 @@ """Tests for the config_flow of the twinly component.""" +from unittest.mock import patch + from homeassistant import config_entries from homeassistant.components.twinkly.const import ( CONF_ENTRY_HOST, @@ -9,7 +11,6 @@ from homeassistant.components.twinkly.const import ( DOMAIN as TWINKLY_DOMAIN, ) -from tests.async_mock import patch from tests.components.twinkly import TEST_MODEL, ClientMock diff --git a/tests/components/twinkly/test_init.py b/tests/components/twinkly/test_init.py index d9dc4623d5e..3f55d2ffdf0 100644 --- a/tests/components/twinkly/test_init.py +++ b/tests/components/twinkly/test_init.py @@ -1,5 +1,6 @@ """Tests of the initialization of the twinly integration.""" +from unittest.mock import patch from uuid import uuid4 from homeassistant.components.twinkly import async_setup_entry, async_unload_entry @@ -12,7 +13,6 @@ from homeassistant.components.twinkly.const import ( ) from homeassistant.core import HomeAssistant -from tests.async_mock import patch from tests.common import MockConfigEntry from tests.components.twinkly import TEST_HOST, TEST_MODEL, TEST_NAME_ORIGINAL diff --git a/tests/components/twinkly/test_twinkly.py b/tests/components/twinkly/test_twinkly.py index 7f73589512a..c8158354195 100644 --- a/tests/components/twinkly/test_twinkly.py +++ b/tests/components/twinkly/test_twinkly.py @@ -1,6 +1,7 @@ """Tests for the integration of a twinly device.""" from typing import Tuple +from unittest.mock import patch from homeassistant.components.twinkly.const import ( CONF_ENTRY_HOST, @@ -14,7 +15,6 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers.device_registry import DeviceEntry from homeassistant.helpers.entity_registry import RegistryEntry -from tests.async_mock import patch from tests.common import MockConfigEntry from tests.components.twinkly import ( TEST_HOST, @@ -216,7 +216,7 @@ async def _create_entries( entity_id = entity_registry.async_get_entity_id("light", TWINKLY_DOMAIN, client.id) entity = entity_registry.async_get(entity_id) - device = device_registry.async_get_device({(TWINKLY_DOMAIN, client.id)}, set()) + device = device_registry.async_get_device({(TWINKLY_DOMAIN, client.id)}) assert entity is not None assert device is not None diff --git a/tests/components/twitch/test_twitch.py b/tests/components/twitch/test_twitch.py index 33afde2a076..310be91c796 100644 --- a/tests/components/twitch/test_twitch.py +++ b/tests/components/twitch/test_twitch.py @@ -1,4 +1,6 @@ """The tests for an update of the Twitch component.""" +from unittest.mock import MagicMock, patch + from requests import HTTPError from twitch.resources import Channel, Follow, Stream, Subscription, User @@ -6,8 +8,6 @@ from homeassistant.components import sensor from homeassistant.const import CONF_CLIENT_ID from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, patch - ENTITY_ID = "sensor.channel123" CONFIG = { sensor.DOMAIN: { diff --git a/tests/components/uk_transport/test_sensor.py b/tests/components/uk_transport/test_sensor.py index ff6cf3d1142..54bc122aa56 100644 --- a/tests/components/uk_transport/test_sensor.py +++ b/tests/components/uk_transport/test_sensor.py @@ -1,5 +1,6 @@ """The tests for the uk_transport platform.""" import re +from unittest.mock import patch import requests_mock @@ -18,7 +19,6 @@ from homeassistant.components.uk_transport.sensor import ( from homeassistant.setup import async_setup_component from homeassistant.util.dt import now -from tests.async_mock import patch from tests.common import load_fixture BUS_ATCOCODE = "340000368SHE" diff --git a/tests/components/unifi/conftest.py b/tests/components/unifi/conftest.py index 8ce7cef0345..b0491a9fa2a 100644 --- a/tests/components/unifi/conftest.py +++ b/tests/components/unifi/conftest.py @@ -1,7 +1,7 @@ """Fixtures for UniFi methods.""" -import pytest +from unittest.mock import patch -from tests.async_mock import patch +import pytest @pytest.fixture(autouse=True) diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 87233f9983c..88c1cbe586d 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -1,7 +1,9 @@ """Test UniFi config flow.""" +from unittest.mock import patch + import aiounifi -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.unifi.const import ( CONF_ALLOW_BANDWIDTH_SENSORS, CONF_ALLOW_UPTIME_SENSORS, @@ -18,6 +20,7 @@ from homeassistant.components.unifi.const import ( CONF_TRACK_WIRED_CLIENTS, DOMAIN as UNIFI_DOMAIN, ) +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -29,7 +32,6 @@ from homeassistant.const import ( from .test_controller import setup_unifi_integration -from tests.async_mock import patch from tests.common import MockConfigEntry CLIENTS = [{"mac": "00:00:00:00:00:01"}] @@ -183,8 +185,8 @@ async def test_flow_works_multiple_sites(hass, aioclient_mock): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "site" - assert result["data_schema"]({"site": "site name"}) - assert result["data_schema"]({"site": "site2 name"}) + assert result["data_schema"]({"site": "default"}) + assert result["data_schema"]({"site": "site2"}) async def test_flow_fails_site_already_configured(hass, aioclient_mock): @@ -313,6 +315,54 @@ async def test_flow_fails_unknown_problem(hass, aioclient_mock): assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT +async def test_reauth_flow_update_configuration(hass, aioclient_mock): + """Verify reauth flow can update controller configuration.""" + controller = await setup_unifi_integration(hass) + + result = await hass.config_entries.flow.async_init( + UNIFI_DOMAIN, + context={"source": SOURCE_REAUTH}, + data=controller.config_entry, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == SOURCE_USER + + aioclient_mock.get("https://1.2.3.4:1234", status=302) + + aioclient_mock.post( + "https://1.2.3.4:1234/api/login", + json={"data": "login successful", "meta": {"rc": "ok"}}, + headers={"content-type": CONTENT_TYPE_JSON}, + ) + + aioclient_mock.get( + "https://1.2.3.4:1234/api/self/sites", + json={ + "data": [{"desc": "Site name", "name": "site_id", "role": "admin"}], + "meta": {"rc": "ok"}, + }, + headers={"content-type": CONTENT_TYPE_JSON}, + ) + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "new_name", + CONF_PASSWORD: "new_pass", + CONF_PORT: 1234, + CONF_VERIFY_SSL: True, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + assert controller.host == "1.2.3.4" + assert controller.config_entry.data[CONF_CONTROLLER][CONF_USERNAME] == "new_name" + assert controller.config_entry.data[CONF_CONTROLLER][CONF_PASSWORD] == "new_pass" + + async def test_advanced_option_flow(hass): """Test advanced config flow options.""" controller = await setup_unifi_integration( @@ -416,3 +466,109 @@ async def test_simple_option_flow(hass): CONF_TRACK_DEVICES: False, CONF_BLOCK_CLIENT: [CLIENTS[0]["mac"]], } + + +async def test_form_ssdp(hass): + """Test we get the form with ssdp source.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + UNIFI_DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data={ + "friendlyName": "UniFi Dream Machine", + "modelDescription": "UniFi Dream Machine Pro", + "ssdp_location": "http://192.168.208.1:41417/rootDesc.xml", + "serialNumber": "e0:63:da:20:14:a9", + }, + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + context = next( + flow["context"] + for flow in hass.config_entries.flow.async_progress() + if flow["flow_id"] == result["flow_id"] + ) + assert context["title_placeholders"] == { + "host": "192.168.208.1", + "site": "default", + } + + +async def test_form_ssdp_aborts_if_host_already_exists(hass): + """Test we abort if the host is already configured.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain=UNIFI_DOMAIN, + data={"controller": {"host": "192.168.208.1", "site": "site_id"}}, + ) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + UNIFI_DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data={ + "friendlyName": "UniFi Dream Machine", + "modelDescription": "UniFi Dream Machine Pro", + "ssdp_location": "http://192.168.208.1:41417/rootDesc.xml", + "serialNumber": "e0:63:da:20:14:a9", + }, + ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_form_ssdp_aborts_if_serial_already_exists(hass): + """Test we abort if the serial is already configured.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain=UNIFI_DOMAIN, + data={"controller": {"host": "1.2.3.4", "site": "site_id"}}, + unique_id="e0:63:da:20:14:a9", + ) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + UNIFI_DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data={ + "friendlyName": "UniFi Dream Machine", + "modelDescription": "UniFi Dream Machine Pro", + "ssdp_location": "http://192.168.208.1:41417/rootDesc.xml", + "serialNumber": "e0:63:da:20:14:a9", + }, + ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_form_ssdp_gets_form_with_ignored_entry(hass): + """Test we can still setup if there is an ignored entry.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain=UNIFI_DOMAIN, + data={"not_controller_key": None}, + source=config_entries.SOURCE_IGNORE, + ) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + UNIFI_DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data={ + "friendlyName": "UniFi Dream Machine New", + "modelDescription": "UniFi Dream Machine Pro", + "ssdp_location": "http://1.2.3.4:41417/rootDesc.xml", + "serialNumber": "e0:63:da:20:14:a9", + }, + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + context = next( + flow["context"] + for flow in hass.config_entries.flow.async_progress() + if flow["flow_id"] == result["flow_id"] + ) + assert context["title_placeholders"] == { + "host": "1.2.3.4", + "site": "default", + } diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 8d5cb85bf9f..6acd507eaad 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -2,6 +2,7 @@ from collections import deque from copy import deepcopy from datetime import timedelta +from unittest.mock import patch import aiounifi import pytest @@ -35,7 +36,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import MockConfigEntry CONTROLLER_HOST = { @@ -197,7 +197,7 @@ async def test_controller_setup(hass): assert controller.option_track_devices == DEFAULT_TRACK_DEVICES assert controller.option_track_wired_clients == DEFAULT_TRACK_WIRED_CLIENTS assert controller.option_detection_time == timedelta(seconds=DEFAULT_DETECTION_TIME) - assert isinstance(controller.option_ssid_filter, list) + assert isinstance(controller.option_ssid_filter, set) assert controller.mac is None @@ -222,6 +222,17 @@ async def test_controller_not_accessible(hass): assert hass.data[UNIFI_DOMAIN] == {} +async def test_controller_trigger_reauth_flow(hass): + """Failed authentication trigger a reauthentication flow.""" + with patch( + "homeassistant.components.unifi.controller.get_controller", + side_effect=AuthenticationRequired, + ), patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: + await setup_unifi_integration(hass) + mock_flow_init.assert_called_once() + assert hass.data[UNIFI_DOMAIN] == {} + + async def test_controller_unknown_error(hass): """Unknown errors are handled.""" with patch( @@ -319,6 +330,14 @@ async def test_get_controller_controller_unavailable(hass): await get_controller(hass, **CONTROLLER_DATA) +async def test_get_controller_login_required(hass): + """Check that get_controller can handle unknown errors.""" + with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( + "aiounifi.Controller.login", side_effect=aiounifi.LoginRequired + ), pytest.raises(AuthenticationRequired): + await get_controller(hass, **CONTROLLER_DATA) + + async def test_get_controller_unknown_error(hass): """Check that get_controller can handle unknown errors.""" with patch("aiounifi.Controller.check_unifi_os", return_value=True), patch( diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 3fef8a16d68..6462fcba943 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -1,6 +1,7 @@ """The tests for the UniFi device tracker platform.""" from copy import copy from datetime import timedelta +from unittest.mock import patch from aiounifi.controller import ( MESSAGE_CLIENT, @@ -188,6 +189,10 @@ async def test_tracked_wireless_clients(hass): client_1 = hass.states.get("device_tracker.client_1") assert client_1.state == "home" + assert client_1.attributes["ip"] == "10.0.0.1" + assert client_1.attributes["mac"] == "00:00:00:00:00:01" + assert client_1.attributes["hostname"] == "client_1" + assert client_1.attributes["host_name"] == "client_1" # State change signalling works with events controller.api.websocket._data = { @@ -200,8 +205,10 @@ async def test_tracked_wireless_clients(hass): client_1 = hass.states.get("device_tracker.client_1") assert client_1.state == "home" - async_fire_time_changed(hass, dt_util.utcnow() + controller.option_detection_time) - await hass.async_block_till_done() + new_time = dt_util.utcnow() + controller.option_detection_time + with patch("homeassistant.util.dt.utcnow", return_value=new_time): + async_fire_time_changed(hass, new_time) + await hass.async_block_till_done() client_1 = hass.states.get("device_tracker.client_1") assert client_1.state == "not_home" @@ -294,8 +301,10 @@ async def test_tracked_devices(hass): device_2 = hass.states.get("device_tracker.device_2") assert device_2.state == "home" - async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=90)) - await hass.async_block_till_done() + new_time = dt_util.utcnow() + timedelta(seconds=90) + with patch("homeassistant.util.dt.utcnow", return_value=new_time): + async_fire_time_changed(hass, new_time) + await hass.async_block_till_done() device_1 = hass.states.get("device_tracker.device_1") assert device_1.state == "not_home" @@ -367,6 +376,12 @@ async def test_controller_state_change(hass): ) assert len(hass.states.async_entity_ids(TRACKER_DOMAIN)) == 2 + client_1 = hass.states.get("device_tracker.client_1") + assert client_1.state == "not_home" + + device_1 = hass.states.get("device_tracker.device_1") + assert device_1.state == "home" + # Controller unavailable controller.async_unifi_signalling_callback( SIGNAL_CONNECTION_STATE, STATE_DISCONNECTED @@ -384,7 +399,7 @@ async def test_controller_state_change(hass): await hass.async_block_till_done() client_1 = hass.states.get("device_tracker.client_1") - assert client_1.state == "home" + assert client_1.state == "not_home" device_1 = hass.states.get("device_tracker.device_1") assert device_1.state == "home" @@ -597,6 +612,7 @@ async def test_option_ssid_filter(hass): controller.config_entry, options={CONF_SSID_FILTER: []}, ) + await hass.async_block_till_done() event = {"meta": {"message": MESSAGE_CLIENT}, "data": [client_1_copy]} controller.api.message_handler(event) event = {"meta": {"message": MESSAGE_CLIENT}, "data": [client_3_copy]} @@ -609,12 +625,17 @@ async def test_option_ssid_filter(hass): client_3 = hass.states.get("device_tracker.client_3") assert client_3.state == "home" - async_fire_time_changed(hass, dt_util.utcnow() + controller.option_detection_time) - await hass.async_block_till_done() + new_time = dt_util.utcnow() + controller.option_detection_time + with patch("homeassistant.util.dt.utcnow", return_value=new_time): + async_fire_time_changed(hass, new_time) + await hass.async_block_till_done() client_1 = hass.states.get("device_tracker.client_1") assert client_1.state == "not_home" + event = {"meta": {"message": MESSAGE_CLIENT}, "data": [client_3_copy]} + controller.api.message_handler(event) + await hass.async_block_till_done() # Client won't go away until after next update client_3 = hass.states.get("device_tracker.client_3") assert client_3.state == "home" @@ -622,9 +643,15 @@ async def test_option_ssid_filter(hass): # Trigger update to get client marked as away event = {"meta": {"message": MESSAGE_CLIENT}, "data": [CLIENT_3]} controller.api.message_handler(event) - async_fire_time_changed(hass, dt_util.utcnow() + controller.option_detection_time) await hass.async_block_till_done() + new_time = ( + dt_util.utcnow() + controller.option_detection_time + timedelta(seconds=1) + ) + with patch("homeassistant.util.dt.utcnow", return_value=new_time): + async_fire_time_changed(hass, new_time) + await hass.async_block_till_done() + client_3 = hass.states.get("device_tracker.client_3") assert client_3.state == "not_home" @@ -658,8 +685,10 @@ async def test_wireless_client_go_wired_issue(hass): assert client_1.attributes["is_wired"] is False # Pass time - async_fire_time_changed(hass, dt_util.utcnow() + controller.option_detection_time) - await hass.async_block_till_done() + new_time = dt_util.utcnow() + controller.option_detection_time + with patch("homeassistant.util.dt.utcnow", return_value=new_time): + async_fire_time_changed(hass, new_time) + await hass.async_block_till_done() # Marked as home according to the timer client_1 = hass.states.get("device_tracker.client_1") @@ -716,8 +745,10 @@ async def test_option_ignore_wired_bug(hass): assert client_1.attributes["is_wired"] is True # pass time - async_fire_time_changed(hass, dt_util.utcnow() + controller.option_detection_time) - await hass.async_block_till_done() + new_time = dt_util.utcnow() + controller.option_detection_time + with patch("homeassistant.util.dt.utcnow", return_value=new_time): + async_fire_time_changed(hass, new_time) + await hass.async_block_till_done() # Timer marks client as away client_1 = hass.states.get("device_tracker.client_1") diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index 80e3e07fa17..cc2a4b3e4a3 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -1,5 +1,5 @@ """Test UniFi setup process.""" -from unittest.mock import Mock, patch +from unittest.mock import AsyncMock, Mock, patch from homeassistant.components import unifi from homeassistant.components.unifi.const import DOMAIN as UNIFI_DOMAIN @@ -7,7 +7,6 @@ from homeassistant.setup import async_setup_component from .test_controller import setup_unifi_integration -from tests.async_mock import AsyncMock from tests.common import MockConfigEntry, mock_coro diff --git a/tests/components/unifi_direct/test_device_tracker.py b/tests/components/unifi_direct/test_device_tracker.py index be58efa415a..1f96e3fd84b 100644 --- a/tests/components/unifi_direct/test_device_tracker.py +++ b/tests/components/unifi_direct/test_device_tracker.py @@ -1,6 +1,7 @@ """The tests for the Unifi direct device tracker platform.""" from datetime import timedelta import os +from unittest.mock import MagicMock, call, patch import pytest import voluptuous as vol @@ -22,7 +23,6 @@ from homeassistant.components.unifi_direct.device_tracker import ( from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, call, patch from tests.common import assert_setup_component, load_fixture, mock_component scanner_path = "homeassistant.components.unifi_direct.device_tracker.UnifiDeviceScanner" diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index 76a397496ad..fd75620f318 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -3,6 +3,7 @@ import asyncio from copy import copy from os import path import unittest +from unittest.mock import patch from voluptuous.error import MultipleInvalid @@ -23,7 +24,6 @@ from homeassistant.const import ( from homeassistant.core import Context, callback from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import get_test_home_assistant, mock_service diff --git a/tests/components/upb/test_config_flow.py b/tests/components/upb/test_config_flow.py index 0c874399f88..779c66f08aa 100644 --- a/tests/components/upb/test_config_flow.py +++ b/tests/components/upb/test_config_flow.py @@ -1,10 +1,10 @@ """Test the UPB Control config flow.""" +from unittest.mock import MagicMock, PropertyMock, patch + from homeassistant import config_entries, setup from homeassistant.components.upb.const import DOMAIN -from tests.async_mock import MagicMock, PropertyMock, patch - def mocked_upb(sync_complete=True, config_ok=True): """Mock UPB lib.""" diff --git a/tests/components/updater/test_init.py b/tests/components/updater/test_init.py index 89ebf9e1bbb..75ba6c1abd5 100644 --- a/tests/components/updater/test_init.py +++ b/tests/components/updater/test_init.py @@ -1,11 +1,12 @@ """The tests for the Updater component.""" +from unittest.mock import patch + import pytest from homeassistant.components import updater from homeassistant.helpers.update_coordinator import UpdateFailed from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import mock_component NEW_VERSION = "10000.0" diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py index 997811a835f..be7794ce8e9 100644 --- a/tests/components/upnp/test_config_flow.py +++ b/tests/components/upnp/test_config_flow.py @@ -1,6 +1,7 @@ """Test UPnP/IGD config flow.""" from datetime import timedelta +from unittest.mock import AsyncMock, patch from homeassistant import config_entries, data_entry_flow from homeassistant.components import ssdp @@ -21,7 +22,6 @@ from homeassistant.setup import async_setup_component from .mock_device import MockDevice -from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index 960d6dacfe5..4373e175bc9 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -1,5 +1,7 @@ """Test UPnP/IGD setup process.""" +from unittest.mock import AsyncMock, patch + from homeassistant.components import upnp from homeassistant.components.upnp.const import ( DISCOVERY_LOCATION, @@ -12,7 +14,6 @@ from homeassistant.setup import async_setup_component from .mock_device import MockDevice -from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry diff --git a/tests/components/usgs_earthquakes_feed/test_geo_location.py b/tests/components/usgs_earthquakes_feed/test_geo_location.py index cfd8ae40bcd..ee845701d81 100644 --- a/tests/components/usgs_earthquakes_feed/test_geo_location.py +++ b/tests/components/usgs_earthquakes_feed/test_geo_location.py @@ -1,5 +1,6 @@ """The tests for the USGS Earthquake Hazards Program Feed platform.""" import datetime +from unittest.mock import MagicMock, call, patch from homeassistant.components import geo_location from homeassistant.components.geo_location import ATTR_SOURCE @@ -31,7 +32,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import MagicMock, call, patch from tests.common import assert_setup_component, async_fire_time_changed CONFIG = { diff --git a/tests/components/utility_meter/test_init.py b/tests/components/utility_meter/test_init.py index c3a5d829a05..c422d3b5c1f 100644 --- a/tests/components/utility_meter/test_init.py +++ b/tests/components/utility_meter/test_init.py @@ -1,5 +1,6 @@ """The tests for the utility_meter component.""" from datetime import timedelta +from unittest.mock import patch from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.utility_meter.const import ( @@ -18,8 +19,6 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch - async def test_services(hass): """Test energy sensor reset service.""" diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 17459912175..24938b1e818 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -1,6 +1,7 @@ """The tests for the utility_meter sensor platform.""" from contextlib import contextmanager from datetime import timedelta +from unittest.mock import patch from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.utility_meter.const import ( @@ -26,7 +27,6 @@ from homeassistant.core import State from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import async_fire_time_changed, mock_restore_cache diff --git a/tests/components/uvc/test_camera.py b/tests/components/uvc/test_camera.py index 35e6c82ded6..00c827b9973 100644 --- a/tests/components/uvc/test_camera.py +++ b/tests/components/uvc/test_camera.py @@ -1,4 +1,5 @@ """The tests for UVC camera module.""" +from datetime import datetime import socket import unittest from unittest import mock @@ -197,10 +198,15 @@ class TestUVC(unittest.TestCase): self.uvc = uvc.UnifiVideoCamera(self.nvr, self.uuid, self.name, self.password) self.nvr.get_camera.return_value = { "model": "UVC Fake", - "recordingSettings": {"fullTimeRecordEnabled": True}, + "uuid": "06e3ff29-8048-31c2-8574-0852d1bd0e03", + "recordingSettings": { + "fullTimeRecordEnabled": True, + "motionRecordEnabled": False, + }, "host": "host-a", "internalHost": "host-b", "username": "admin", + "lastRecordingStartTime": 1610070992367, "channels": [ { "id": "0", @@ -238,6 +244,30 @@ class TestUVC(unittest.TestCase): assert "Ubiquiti" == self.uvc.brand assert "UVC Fake" == self.uvc.model assert SUPPORT_STREAM == self.uvc.supported_features + assert "uuid" == self.uvc.unique_id + + def test_motion_recording_mode_properties(self): + """Test the properties.""" + self.nvr.get_camera.return_value["recordingSettings"][ + "fullTimeRecordEnabled" + ] = False + self.nvr.get_camera.return_value["recordingSettings"][ + "motionRecordEnabled" + ] = True + assert not self.uvc.is_recording + assert ( + datetime(2021, 1, 8, 1, 56, 32, 367000) + == self.uvc.state_attributes["last_recording_start_time"] + ) + + self.nvr.get_camera.return_value["recordingIndicator"] = "DISABLED" + assert not self.uvc.is_recording + + self.nvr.get_camera.return_value["recordingIndicator"] = "MOTION_INPROGRESS" + assert self.uvc.is_recording + + self.nvr.get_camera.return_value["recordingIndicator"] = "MOTION_FINISHED" + assert self.uvc.is_recording def test_stream(self): """Test the RTSP stream URI.""" diff --git a/tests/components/velbus/test_config_flow.py b/tests/components/velbus/test_config_flow.py index 0a7bc4489c7..f4a95f0fdf9 100644 --- a/tests/components/velbus/test_config_flow.py +++ b/tests/components/velbus/test_config_flow.py @@ -1,11 +1,12 @@ """Tests for the Velbus config flow.""" +from unittest.mock import Mock, patch + import pytest from homeassistant import data_entry_flow from homeassistant.components.velbus import config_flow from homeassistant.const import CONF_NAME, CONF_PORT -from tests.async_mock import Mock, patch from tests.common import MockConfigEntry PORT_SERIAL = "/dev/ttyACME100" diff --git a/tests/components/vera/common.py b/tests/components/vera/common.py index 29c6a0e8683..43ef154b588 100644 --- a/tests/components/vera/common.py +++ b/tests/components/vera/common.py @@ -1,6 +1,7 @@ """Common code for tests.""" from enum import Enum from typing import Callable, Dict, NamedTuple, Tuple +from unittest.mock import MagicMock import pyvera as pv @@ -13,7 +14,6 @@ from homeassistant.components.vera.const import ( from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock from tests.common import MockConfigEntry SetupCallback = Callable[[pv.VeraController, dict], None] diff --git a/tests/components/vera/conftest.py b/tests/components/vera/conftest.py index c20dc4ed499..da027207748 100644 --- a/tests/components/vera/conftest.py +++ b/tests/components/vera/conftest.py @@ -1,9 +1,10 @@ """Fixtures for tests.""" +from unittest.mock import patch + import pytest from .common import ComponentFactory -from tests.async_mock import patch from tests.components.light.conftest import mock_light_profiles # noqa diff --git a/tests/components/vera/test_binary_sensor.py b/tests/components/vera/test_binary_sensor.py index a02c2ef1635..1bcb8d1a183 100644 --- a/tests/components/vera/test_binary_sensor.py +++ b/tests/components/vera/test_binary_sensor.py @@ -1,12 +1,12 @@ """Vera tests.""" +from unittest.mock import MagicMock + import pyvera as pv from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config -from tests.async_mock import MagicMock - async def test_binary_sensor( hass: HomeAssistant, vera_component_factory: ComponentFactory diff --git a/tests/components/vera/test_climate.py b/tests/components/vera/test_climate.py index 370ecc18dcd..076b51997a0 100644 --- a/tests/components/vera/test_climate.py +++ b/tests/components/vera/test_climate.py @@ -1,4 +1,6 @@ """Vera tests.""" +from unittest.mock import MagicMock + import pyvera as pv from homeassistant.components.climate.const import ( @@ -13,8 +15,6 @@ from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config -from tests.async_mock import MagicMock - async def test_climate( hass: HomeAssistant, vera_component_factory: ComponentFactory diff --git a/tests/components/vera/test_common.py b/tests/components/vera/test_common.py index 509bbc5f96a..3832daf0710 100644 --- a/tests/components/vera/test_common.py +++ b/tests/components/vera/test_common.py @@ -1,11 +1,11 @@ """Tests for common vera code.""" from datetime import timedelta +from unittest.mock import MagicMock from homeassistant.components.vera import SubscriptionRegistry from homeassistant.core import HomeAssistant from homeassistant.util.dt import utcnow -from tests.async_mock import MagicMock from tests.common import async_fire_time_changed diff --git a/tests/components/vera/test_config_flow.py b/tests/components/vera/test_config_flow.py index dceac728e4d..780583e38ab 100644 --- a/tests/components/vera/test_config_flow.py +++ b/tests/components/vera/test_config_flow.py @@ -1,4 +1,6 @@ """Vera tests.""" +from unittest.mock import MagicMock, patch + from requests.exceptions import RequestException from homeassistant import config_entries, data_entry_flow @@ -7,7 +9,6 @@ from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry, mock_registry diff --git a/tests/components/vera/test_cover.py b/tests/components/vera/test_cover.py index f3dc2263749..0c05d84e2db 100644 --- a/tests/components/vera/test_cover.py +++ b/tests/components/vera/test_cover.py @@ -1,12 +1,12 @@ """Vera tests.""" +from unittest.mock import MagicMock + import pyvera as pv from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config -from tests.async_mock import MagicMock - async def test_cover( hass: HomeAssistant, vera_component_factory: ComponentFactory diff --git a/tests/components/vera/test_init.py b/tests/components/vera/test_init.py index b3f7b3249ef..b1d6010336a 100644 --- a/tests/components/vera/test_init.py +++ b/tests/components/vera/test_init.py @@ -1,4 +1,6 @@ """Vera tests.""" +from unittest.mock import MagicMock + import pytest import pyvera as pv from requests.exceptions import RequestException @@ -14,7 +16,6 @@ from homeassistant.core import HomeAssistant from .common import ComponentFactory, ConfigSource, new_simple_controller_config -from tests.async_mock import MagicMock from tests.common import MockConfigEntry, mock_registry diff --git a/tests/components/vera/test_light.py b/tests/components/vera/test_light.py index 72118e33a31..3b14aba7429 100644 --- a/tests/components/vera/test_light.py +++ b/tests/components/vera/test_light.py @@ -1,4 +1,6 @@ """Vera tests.""" +from unittest.mock import MagicMock + import pyvera as pv from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_HS_COLOR @@ -6,8 +8,6 @@ from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config -from tests.async_mock import MagicMock - async def test_light( hass: HomeAssistant, vera_component_factory: ComponentFactory diff --git a/tests/components/vera/test_lock.py b/tests/components/vera/test_lock.py index b3433b2bafb..c288ac8709e 100644 --- a/tests/components/vera/test_lock.py +++ b/tests/components/vera/test_lock.py @@ -1,4 +1,6 @@ """Vera tests.""" +from unittest.mock import MagicMock + import pyvera as pv from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED @@ -6,8 +8,6 @@ from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config -from tests.async_mock import MagicMock - async def test_lock( hass: HomeAssistant, vera_component_factory: ComponentFactory diff --git a/tests/components/vera/test_scene.py b/tests/components/vera/test_scene.py index 6c80f27d8c8..2d4b7375498 100644 --- a/tests/components/vera/test_scene.py +++ b/tests/components/vera/test_scene.py @@ -1,12 +1,12 @@ """Vera tests.""" +from unittest.mock import MagicMock + import pyvera as pv from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config -from tests.async_mock import MagicMock - async def test_scene( hass: HomeAssistant, vera_component_factory: ComponentFactory diff --git a/tests/components/vera/test_sensor.py b/tests/components/vera/test_sensor.py index 3d6b11b0685..43777642816 100644 --- a/tests/components/vera/test_sensor.py +++ b/tests/components/vera/test_sensor.py @@ -1,5 +1,6 @@ """Vera tests.""" from typing import Any, Callable, Tuple +from unittest.mock import MagicMock import pyvera as pv @@ -8,8 +9,6 @@ from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config -from tests.async_mock import MagicMock - async def run_sensor_test( hass: HomeAssistant, diff --git a/tests/components/vera/test_switch.py b/tests/components/vera/test_switch.py index 42c74e4e843..b61564c56bc 100644 --- a/tests/components/vera/test_switch.py +++ b/tests/components/vera/test_switch.py @@ -1,12 +1,12 @@ """Vera tests.""" +from unittest.mock import MagicMock + import pyvera as pv from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config -from tests.async_mock import MagicMock - async def test_switch( hass: HomeAssistant, vera_component_factory: ComponentFactory diff --git a/tests/components/verisure/test_ethernet_status.py b/tests/components/verisure/test_ethernet_status.py index 139ac01a1c6..611adde19d9 100644 --- a/tests/components/verisure/test_ethernet_status.py +++ b/tests/components/verisure/test_ethernet_status.py @@ -1,12 +1,11 @@ """Test Verisure ethernet status.""" from contextlib import contextmanager +from unittest.mock import patch from homeassistant.components.verisure import DOMAIN as VERISURE_DOMAIN from homeassistant.const import STATE_UNAVAILABLE from homeassistant.setup import async_setup_component -from tests.async_mock import patch - CONFIG = { "verisure": { "username": "test", diff --git a/tests/components/verisure/test_lock.py b/tests/components/verisure/test_lock.py index decce67dc11..d41bbab2037 100644 --- a/tests/components/verisure/test_lock.py +++ b/tests/components/verisure/test_lock.py @@ -1,6 +1,7 @@ """Tests for the Verisure platform.""" from contextlib import contextmanager +from unittest.mock import call, patch from homeassistant.components.lock import ( DOMAIN as LOCK_DOMAIN, @@ -11,8 +12,6 @@ from homeassistant.components.verisure import DOMAIN as VERISURE_DOMAIN from homeassistant.const import STATE_UNLOCKED from homeassistant.setup import async_setup_component -from tests.async_mock import call, patch - NO_DEFAULT_LOCK_CODE_CONFIG = { "verisure": { "username": "test", diff --git a/tests/components/version/test_sensor.py b/tests/components/version/test_sensor.py index 471043ae3ae..164b4090e5f 100644 --- a/tests/components/version/test_sensor.py +++ b/tests/components/version/test_sensor.py @@ -1,7 +1,7 @@ """The test for the version sensor platform.""" -from homeassistant.setup import async_setup_component +from unittest.mock import patch -from tests.async_mock import patch +from homeassistant.setup import async_setup_component MOCK_VERSION = "10.0" diff --git a/tests/components/vesync/test_config_flow.py b/tests/components/vesync/test_config_flow.py index c82307d351c..f302d0ca5b3 100644 --- a/tests/components/vesync/test_config_flow.py +++ b/tests/components/vesync/test_config_flow.py @@ -1,9 +1,10 @@ """Test for vesync config flow.""" +from unittest.mock import patch + from homeassistant import data_entry_flow from homeassistant.components.vesync import DOMAIN, config_flow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/vilfo/test_config_flow.py b/tests/components/vilfo/test_config_flow.py index ca77b199cfa..6e98ef3fdd9 100644 --- a/tests/components/vilfo/test_config_flow.py +++ b/tests/components/vilfo/test_config_flow.py @@ -1,12 +1,12 @@ """Test the Vilfo Router config flow.""" +from unittest.mock import Mock, patch + import vilfo from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.vilfo.const import DOMAIN from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, CONF_ID, CONF_MAC -from tests.async_mock import Mock, patch - async def test_form(hass): """Test we get the form.""" diff --git a/tests/components/vizio/conftest.py b/tests/components/vizio/conftest.py index c8a9083bb1a..917e6f7f291 100644 --- a/tests/components/vizio/conftest.py +++ b/tests/components/vizio/conftest.py @@ -1,4 +1,6 @@ """Configure py.test.""" +from unittest.mock import AsyncMock, patch + import pytest from pyvizio.api.apps import AppConfig from pyvizio.const import DEVICE_CLASS_SPEAKER, MAX_VOLUME @@ -22,8 +24,6 @@ from .const import ( MockStartPairingResponse, ) -from tests.async_mock import AsyncMock, patch - class MockInput: """Mock Vizio device input.""" diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py index 0d11ec2289c..78976032b00 100644 --- a/tests/components/vizio/test_media_player.py +++ b/tests/components/vizio/test_media_player.py @@ -2,6 +2,7 @@ from contextlib import asynccontextmanager from datetime import timedelta from typing import Any, Dict, List, Optional +from unittest.mock import call, patch import pytest from pytest import raises @@ -75,7 +76,6 @@ from .const import ( VOLUME_STEP, ) -from tests.async_mock import call, patch from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/volumio/test_config_flow.py b/tests/components/volumio/test_config_flow.py index 477ef25cb87..86b5027cd3d 100644 --- a/tests/components/volumio/test_config_flow.py +++ b/tests/components/volumio/test_config_flow.py @@ -1,9 +1,10 @@ """Test the Volumio config flow.""" +from unittest.mock import patch + from homeassistant import config_entries from homeassistant.components.volumio.config_flow import CannotConnectError from homeassistant.components.volumio.const import DOMAIN -from tests.async_mock import patch from tests.common import MockConfigEntry TEST_SYSTEM_INFO = {"id": "1111-1111-1111-1111", "name": "TestVolumio"} diff --git a/tests/components/vultr/test_binary_sensor.py b/tests/components/vultr/test_binary_sensor.py index af99dc12c5f..ab12bfda12c 100644 --- a/tests/components/vultr/test_binary_sensor.py +++ b/tests/components/vultr/test_binary_sensor.py @@ -1,6 +1,7 @@ """Test the Vultr binary sensor platform.""" import json import unittest +from unittest.mock import patch import pytest import requests_mock @@ -19,7 +20,6 @@ from homeassistant.components.vultr import ( ) from homeassistant.const import CONF_NAME, CONF_PLATFORM -from tests.async_mock import patch from tests.common import get_test_home_assistant, load_fixture from tests.components.vultr.test_init import VALID_CONFIG diff --git a/tests/components/vultr/test_init.py b/tests/components/vultr/test_init.py index 9ce96d7969c..80480a2cec2 100644 --- a/tests/components/vultr/test_init.py +++ b/tests/components/vultr/test_init.py @@ -2,13 +2,13 @@ from copy import deepcopy import json import unittest +from unittest.mock import patch import requests_mock from homeassistant import setup import homeassistant.components.vultr as vultr -from tests.async_mock import patch from tests.common import get_test_home_assistant, load_fixture VALID_CONFIG = {"vultr": {"api_key": "ABCDEFG1234567"}} diff --git a/tests/components/vultr/test_sensor.py b/tests/components/vultr/test_sensor.py index 8e9cb606d1e..2b5e07c80b5 100644 --- a/tests/components/vultr/test_sensor.py +++ b/tests/components/vultr/test_sensor.py @@ -1,6 +1,7 @@ """The tests for the Vultr sensor platform.""" import json import unittest +from unittest.mock import patch import pytest import requests_mock @@ -16,7 +17,6 @@ from homeassistant.const import ( DATA_GIGABYTES, ) -from tests.async_mock import patch from tests.common import get_test_home_assistant, load_fixture from tests.components.vultr.test_init import VALID_CONFIG diff --git a/tests/components/vultr/test_switch.py b/tests/components/vultr/test_switch.py index 77eb1e7a8c6..12af400a44a 100644 --- a/tests/components/vultr/test_switch.py +++ b/tests/components/vultr/test_switch.py @@ -1,6 +1,7 @@ """Test the Vultr switch platform.""" import json import unittest +from unittest.mock import patch import pytest import requests_mock @@ -19,7 +20,6 @@ from homeassistant.components.vultr import ( ) from homeassistant.const import CONF_NAME, CONF_PLATFORM -from tests.async_mock import patch from tests.common import get_test_home_assistant, load_fixture from tests.components.vultr.test_init import VALID_CONFIG diff --git a/tests/components/wake_on_lan/test_init.py b/tests/components/wake_on_lan/test_init.py index 60a725a7f9e..3ec7a53a436 100644 --- a/tests/components/wake_on_lan/test_init.py +++ b/tests/components/wake_on_lan/test_init.py @@ -1,12 +1,12 @@ """Tests for Wake On LAN component.""" +from unittest.mock import patch + import pytest import voluptuous as vol from homeassistant.components.wake_on_lan import DOMAIN, SERVICE_SEND_MAGIC_PACKET from homeassistant.setup import async_setup_component -from tests.async_mock import patch - async def test_send_magic_packet(hass): """Test of send magic packet service call.""" diff --git a/tests/components/wake_on_lan/test_switch.py b/tests/components/wake_on_lan/test_switch.py index 598eb3a522e..c2e32f77ccf 100644 --- a/tests/components/wake_on_lan/test_switch.py +++ b/tests/components/wake_on_lan/test_switch.py @@ -1,6 +1,7 @@ """The tests for the wake on lan switch platform.""" import platform import subprocess +from unittest.mock import patch import pytest @@ -14,7 +15,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.common import async_mock_service diff --git a/tests/components/webhook/test_trigger.py b/tests/components/webhook/test_trigger.py index 46459bd88c4..2a22c330e14 100644 --- a/tests/components/webhook/test_trigger.py +++ b/tests/components/webhook/test_trigger.py @@ -1,10 +1,11 @@ """The tests for the webhook automation trigger.""" +from unittest.mock import patch + import pytest from homeassistant.core import callback from homeassistant.setup import async_setup_component -from tests.async_mock import patch from tests.components.blueprint.conftest import stub_blueprint_populate # noqa diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index 1b85daa1922..70bc8274684 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -1,5 +1,6 @@ """The tests for the LG webOS media player platform.""" -import sys + +from unittest.mock import patch import pytest @@ -25,12 +26,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -if sys.version_info >= (3, 8, 0): - from tests.async_mock import patch -else: - from tests.async_mock import patch - - NAME = "fake" ENTITY_ID = f"{media_player.DOMAIN}.{NAME}" diff --git a/tests/components/websocket_api/test_commands.py b/tests/components/websocket_api/test_commands.py index 721d178430e..a7aa17db6d3 100644 --- a/tests/components/websocket_api/test_commands.py +++ b/tests/components/websocket_api/test_commands.py @@ -1,5 +1,6 @@ """Tests for WebSocket API commands.""" from async_timeout import timeout +import voluptuous as vol from homeassistant.components.websocket_api import const from homeassistant.components.websocket_api.auth import ( @@ -11,6 +12,7 @@ from homeassistant.components.websocket_api.const import URL from homeassistant.core import Context, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import async_get_integration from homeassistant.setup import async_setup_component @@ -94,6 +96,77 @@ async def test_call_service_child_not_found(hass, websocket_client): assert msg["error"]["code"] == const.ERR_HOME_ASSISTANT_ERROR +async def test_call_service_schema_validation_error( + hass: HomeAssistantType, websocket_client +): + """Test call service command with invalid service data.""" + + calls = [] + service_schema = vol.Schema( + { + vol.Required("message"): str, + } + ) + + @callback + def service_call(call): + calls.append(call) + + hass.services.async_register( + "domain_test", + "test_service", + service_call, + schema=service_schema, + ) + + await websocket_client.send_json( + { + "id": 5, + "type": "call_service", + "domain": "domain_test", + "service": "test_service", + "service_data": {}, + } + ) + msg = await websocket_client.receive_json() + assert msg["id"] == 5 + assert msg["type"] == const.TYPE_RESULT + assert not msg["success"] + assert msg["error"]["code"] == const.ERR_INVALID_FORMAT + + await websocket_client.send_json( + { + "id": 6, + "type": "call_service", + "domain": "domain_test", + "service": "test_service", + "service_data": {"extra_key": "not allowed"}, + } + ) + msg = await websocket_client.receive_json() + assert msg["id"] == 6 + assert msg["type"] == const.TYPE_RESULT + assert not msg["success"] + assert msg["error"]["code"] == const.ERR_INVALID_FORMAT + + await websocket_client.send_json( + { + "id": 7, + "type": "call_service", + "domain": "domain_test", + "service": "test_service", + "service_data": {"message": []}, + } + ) + msg = await websocket_client.receive_json() + assert msg["id"] == 7 + assert msg["type"] == const.TYPE_RESULT + assert not msg["success"] + assert msg["error"]["code"] == const.ERR_INVALID_FORMAT + + assert len(calls) == 0 + + async def test_call_service_error(hass, websocket_client): """Test call service command with error.""" diff --git a/tests/components/websocket_api/test_http.py b/tests/components/websocket_api/test_http.py index e76cbe0dbdc..d3cf4b854f8 100644 --- a/tests/components/websocket_api/test_http.py +++ b/tests/components/websocket_api/test_http.py @@ -1,5 +1,6 @@ """Test Websocket API http module.""" from datetime import timedelta +from unittest.mock import patch from aiohttp import WSMsgType import pytest @@ -7,7 +8,6 @@ import pytest from homeassistant.components.websocket_api import const, http from homeassistant.util.dt import utcnow -from tests.async_mock import patch from tests.common import async_fire_time_changed diff --git a/tests/components/websocket_api/test_init.py b/tests/components/websocket_api/test_init.py index 2d656de8eeb..041c0e76533 100644 --- a/tests/components/websocket_api/test_init.py +++ b/tests/components/websocket_api/test_init.py @@ -1,11 +1,11 @@ """Tests for the Home Assistant Websocket API.""" +from unittest.mock import Mock, patch + from aiohttp import WSMsgType import voluptuous as vol from homeassistant.components.websocket_api import const, messages -from tests.async_mock import Mock, patch - async def test_invalid_message_format(websocket_client): """Test sending invalid JSON.""" diff --git a/tests/components/wemo/conftest.py b/tests/components/wemo/conftest.py index 573de21c692..0e0a69216b2 100644 --- a/tests/components/wemo/conftest.py +++ b/tests/components/wemo/conftest.py @@ -1,5 +1,6 @@ """Fixtures for pywemo.""" import asyncio +from unittest.mock import create_autospec, patch import pytest import pywemo @@ -8,8 +9,6 @@ from homeassistant.components.wemo import CONF_DISCOVERY, CONF_STATIC from homeassistant.components.wemo.const import DOMAIN from homeassistant.setup import async_setup_component -from tests.async_mock import create_autospec, patch - MOCK_HOST = "127.0.0.1" MOCK_PORT = 50000 MOCK_NAME = "WemoDeviceName" diff --git a/tests/components/wemo/entity_test_helpers.py b/tests/components/wemo/entity_test_helpers.py index 16a2f8b3f0d..0ecfc46d526 100644 --- a/tests/components/wemo/entity_test_helpers.py +++ b/tests/components/wemo/entity_test_helpers.py @@ -4,6 +4,7 @@ This is not a test module. These test methods are used by the platform test modu """ import asyncio import threading +from unittest.mock import patch from homeassistant.components.homeassistant import ( DOMAIN as HA_DOMAIN, @@ -13,8 +14,6 @@ from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_UNAVAILABLE from homeassistant.core import callback from homeassistant.setup import async_setup_component -from tests.async_mock import patch - def _perform_registry_callback(hass, pywemo_registry, pywemo_device): """Return a callable method to trigger a state callback from the device.""" diff --git a/tests/components/wemo/test_init.py b/tests/components/wemo/test_init.py index 2af91c0fe32..374222d8688 100644 --- a/tests/components/wemo/test_init.py +++ b/tests/components/wemo/test_init.py @@ -1,5 +1,6 @@ """Tests for the wemo component.""" from datetime import timedelta +from unittest.mock import create_autospec, patch import pywemo @@ -10,7 +11,6 @@ from homeassistant.util import dt from .conftest import MOCK_HOST, MOCK_NAME, MOCK_PORT, MOCK_SERIAL_NUMBER -from tests.async_mock import create_autospec, patch from tests.common import async_fire_time_changed diff --git a/tests/components/wemo/test_light_bridge.py b/tests/components/wemo/test_light_bridge.py index 1a36e5421ec..3e7f79200c6 100644 --- a/tests/components/wemo/test_light_bridge.py +++ b/tests/components/wemo/test_light_bridge.py @@ -1,4 +1,6 @@ """Tests for the Wemo light entity via the bridge.""" +from unittest.mock import create_autospec, patch + import pytest import pywemo @@ -13,8 +15,6 @@ import homeassistant.util.dt as dt_util from . import entity_test_helpers -from tests.async_mock import create_autospec, patch - @pytest.fixture def pywemo_model(): diff --git a/tests/components/wiffi/test_config_flow.py b/tests/components/wiffi/test_config_flow.py index cd129d112bc..50433d377a9 100644 --- a/tests/components/wiffi/test_config_flow.py +++ b/tests/components/wiffi/test_config_flow.py @@ -1,5 +1,6 @@ """Test the wiffi integration config flow.""" import errno +from unittest.mock import patch import pytest @@ -12,7 +13,6 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) -from tests.async_mock import patch from tests.common import MockConfigEntry MOCK_CONFIG = {CONF_PORT: 8765} diff --git a/tests/components/wilight/test_config_flow.py b/tests/components/wilight/test_config_flow.py index c8476cbe349..d44780092ec 100644 --- a/tests/components/wilight/test_config_flow.py +++ b/tests/components/wilight/test_config_flow.py @@ -1,4 +1,6 @@ """Test the WiLight config flow.""" +from unittest.mock import patch + import pytest from homeassistant.components.wilight.config_flow import ( @@ -15,12 +17,10 @@ from homeassistant.data_entry_flow import ( ) from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import patch from tests.common import MockConfigEntry from tests.components.wilight import ( CONF_COMPONENTS, HOST, - MOCK_SSDP_DISCOVERY_INFO_LIGHT_FAN, MOCK_SSDP_DISCOVERY_INFO_MISSING_MANUFACTORER, MOCK_SSDP_DISCOVERY_INFO_P_B, MOCK_SSDP_DISCOVERY_INFO_WRONG_MANUFACTORER, @@ -31,7 +31,7 @@ from tests.components.wilight import ( @pytest.fixture(name="dummy_get_components_from_model_clear") -def mock_dummy_get_components_from_model(): +def mock_dummy_get_components_from_model_clear(): """Mock a clear components list.""" components = [] with patch( @@ -41,6 +41,17 @@ def mock_dummy_get_components_from_model(): yield components +@pytest.fixture(name="dummy_get_components_from_model_wrong") +def mock_dummy_get_components_from_model_wrong(): + """Mock a clear components list.""" + components = ["wrong"] + with patch( + "pywilight.get_components_from_model", + return_value=components, + ): + yield components + + async def test_show_ssdp_form(hass: HomeAssistantType) -> None: """Test that the ssdp confirmation form is served.""" @@ -95,10 +106,12 @@ async def test_ssdp_not_wilight_abort_3( assert result["reason"] == "not_wilight_device" -async def test_ssdp_not_supported_abort(hass: HomeAssistantType) -> None: +async def test_ssdp_not_supported_abort( + hass: HomeAssistantType, dummy_get_components_from_model_wrong +) -> None: """Test that the ssdp aborts not_supported.""" - discovery_info = MOCK_SSDP_DISCOVERY_INFO_LIGHT_FAN.copy() + discovery_info = MOCK_SSDP_DISCOVERY_INFO_P_B.copy() result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) diff --git a/tests/components/wilight/test_fan.py b/tests/components/wilight/test_fan.py new file mode 100644 index 00000000000..9b656236b93 --- /dev/null +++ b/tests/components/wilight/test_fan.py @@ -0,0 +1,206 @@ +"""Tests for the WiLight integration.""" +from unittest.mock import patch + +import pytest +import pywilight + +from homeassistant.components.fan import ( + ATTR_DIRECTION, + ATTR_SPEED, + DIRECTION_FORWARD, + DIRECTION_REVERSE, + DOMAIN as FAN_DOMAIN, + SERVICE_SET_DIRECTION, + SERVICE_SET_SPEED, + SPEED_HIGH, + SPEED_LOW, + SPEED_MEDIUM, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) +from homeassistant.helpers.typing import HomeAssistantType + +from . import ( + HOST, + UPNP_MAC_ADDRESS, + UPNP_MODEL_NAME_LIGHT_FAN, + UPNP_MODEL_NUMBER, + UPNP_SERIAL, + WILIGHT_ID, + setup_integration, +) + + +@pytest.fixture(name="dummy_device_from_host_light_fan") +def mock_dummy_device_from_host_light_fan(): + """Mock a valid api_devce.""" + + device = pywilight.wilight_from_discovery( + f"http://{HOST}:45995/wilight.xml", + UPNP_MAC_ADDRESS, + UPNP_MODEL_NAME_LIGHT_FAN, + UPNP_SERIAL, + UPNP_MODEL_NUMBER, + ) + + device.set_dummy(True) + + with patch( + "pywilight.device_from_host", + return_value=device, + ): + yield device + + +async def test_loading_light_fan( + hass: HomeAssistantType, + dummy_device_from_host_light_fan, +) -> None: + """Test the WiLight configuration entry loading.""" + + entry = await setup_integration(hass) + assert entry + assert entry.unique_id == WILIGHT_ID + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + # First segment of the strip + state = hass.states.get("fan.wl000000000099_2") + assert state + assert state.state == STATE_OFF + + entry = entity_registry.async_get("fan.wl000000000099_2") + assert entry + assert entry.unique_id == "WL000000000099_1" + + +async def test_on_off_fan_state( + hass: HomeAssistantType, dummy_device_from_host_light_fan +) -> None: + """Test the change of state of the fan switches.""" + await setup_integration(hass) + + # Turn on + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "fan.wl000000000099_2"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("fan.wl000000000099_2") + assert state + assert state.state == STATE_ON + + # Turn on with speed + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_ON, + {ATTR_SPEED: SPEED_LOW, ATTR_ENTITY_ID: "fan.wl000000000099_2"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("fan.wl000000000099_2") + assert state + assert state.state == STATE_ON + assert state.attributes.get(ATTR_SPEED) == SPEED_LOW + + # Turn off + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "fan.wl000000000099_2"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("fan.wl000000000099_2") + assert state + assert state.state == STATE_OFF + + +async def test_speed_fan_state( + hass: HomeAssistantType, dummy_device_from_host_light_fan +) -> None: + """Test the change of speed of the fan switches.""" + await setup_integration(hass) + + # Set speed Low + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_SPEED, + {ATTR_SPEED: SPEED_LOW, ATTR_ENTITY_ID: "fan.wl000000000099_2"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("fan.wl000000000099_2") + assert state + assert state.attributes.get(ATTR_SPEED) == SPEED_LOW + + # Set speed Medium + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_SPEED, + {ATTR_SPEED: SPEED_MEDIUM, ATTR_ENTITY_ID: "fan.wl000000000099_2"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("fan.wl000000000099_2") + assert state + assert state.attributes.get(ATTR_SPEED) == SPEED_MEDIUM + + # Set speed High + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_SPEED, + {ATTR_SPEED: SPEED_HIGH, ATTR_ENTITY_ID: "fan.wl000000000099_2"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("fan.wl000000000099_2") + assert state + assert state.attributes.get(ATTR_SPEED) == SPEED_HIGH + + +async def test_direction_fan_state( + hass: HomeAssistantType, dummy_device_from_host_light_fan +) -> None: + """Test the change of direction of the fan switches.""" + await setup_integration(hass) + + # Set direction Forward + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_DIRECTION, + {ATTR_DIRECTION: DIRECTION_FORWARD, ATTR_ENTITY_ID: "fan.wl000000000099_2"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("fan.wl000000000099_2") + assert state + assert state.state == STATE_ON + assert state.attributes.get(ATTR_DIRECTION) == DIRECTION_FORWARD + + # Set direction Reverse + await hass.services.async_call( + FAN_DOMAIN, + SERVICE_SET_DIRECTION, + {ATTR_DIRECTION: DIRECTION_REVERSE, ATTR_ENTITY_ID: "fan.wl000000000099_2"}, + blocking=True, + ) + + await hass.async_block_till_done() + state = hass.states.get("fan.wl000000000099_2") + assert state + assert state.attributes.get(ATTR_DIRECTION) == DIRECTION_REVERSE diff --git a/tests/components/wilight/test_init.py b/tests/components/wilight/test_init.py index efd779a29df..c1557fb44d3 100644 --- a/tests/components/wilight/test_init.py +++ b/tests/components/wilight/test_init.py @@ -1,4 +1,6 @@ """Tests for the WiLight integration.""" +from unittest.mock import patch + import pytest import pywilight @@ -10,7 +12,6 @@ from homeassistant.config_entries import ( ) from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import patch from tests.components.wilight import ( HOST, UPNP_MAC_ADDRESS, diff --git a/tests/components/wilight/test_light.py b/tests/components/wilight/test_light.py index d02c7233e60..b7250df546d 100644 --- a/tests/components/wilight/test_light.py +++ b/tests/components/wilight/test_light.py @@ -1,4 +1,6 @@ """Tests for the WiLight integration.""" +from unittest.mock import patch + import pytest import pywilight @@ -16,7 +18,6 @@ from homeassistant.const import ( ) from homeassistant.helpers.typing import HomeAssistantType -from tests.async_mock import patch from tests.components.wilight import ( HOST, UPNP_MAC_ADDRESS, diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index 000900c3355..80e6d07654c 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -1,6 +1,7 @@ """Common data for for the withings component tests.""" from dataclasses import dataclass from typing import List, Optional, Tuple, Union +from unittest.mock import MagicMock from urllib.parse import urlparse from aiohttp.test_utils import TestClient @@ -39,7 +40,6 @@ from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.config_entry_oauth2_flow import AUTH_CALLBACK_PATH from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index 2bf71ab7aa5..a5946ff0533 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -2,6 +2,7 @@ import datetime import re from typing import Any +from unittest.mock import MagicMock from urllib.parse import urlparse from aiohttp.test_utils import TestClient @@ -17,7 +18,6 @@ from homeassistant.components.withings.common import ( from homeassistant.core import HomeAssistant from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2Implementation -from tests.async_mock import MagicMock from tests.common import MockConfigEntry from tests.components.withings.common import ( ComponentFactory, diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py index 4f4a85585bf..a9948860745 100644 --- a/tests/components/withings/test_init.py +++ b/tests/components/withings/test_init.py @@ -1,4 +1,6 @@ """Tests for the Withings component.""" +from unittest.mock import MagicMock, patch + import pytest import voluptuous as vol from withings_api.common import UnauthorizedException @@ -25,7 +27,6 @@ from .common import ( new_profile_config, ) -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py index aed10ca3466..4ed1723be77 100644 --- a/tests/components/wled/test_config_flow.py +++ b/tests/components/wled/test_config_flow.py @@ -1,4 +1,6 @@ """Tests for the WLED config flow.""" +from unittest.mock import MagicMock, patch + import aiohttp from wled import WLEDConnectionError @@ -10,7 +12,6 @@ from homeassistant.core import HomeAssistant from . import init_integration -from tests.async_mock import MagicMock, patch from tests.common import load_fixture from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/wled/test_init.py b/tests/components/wled/test_init.py index 8ed043530ae..edce49cfd80 100644 --- a/tests/components/wled/test_init.py +++ b/tests/components/wled/test_init.py @@ -1,11 +1,12 @@ """Tests for the WLED integration.""" +from unittest.mock import MagicMock, patch + from wled import WLEDConnectionError from homeassistant.components.wled.const import DOMAIN from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY from homeassistant.core import HomeAssistant -from tests.async_mock import MagicMock, patch from tests.components.wled import init_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index fbcc6ca71b6..eb9124ab906 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -1,5 +1,6 @@ """Tests for the WLED light platform.""" import json +from unittest.mock import patch from wled import Device as WLEDDevice, WLEDConnectionError @@ -37,7 +38,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import async_fire_time_changed, load_fixture from tests.components.wled import init_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py index d5c1c738d2f..11e14bd79d9 100644 --- a/tests/components/wled/test_sensor.py +++ b/tests/components/wled/test_sensor.py @@ -1,5 +1,6 @@ """Tests for the WLED sensor platform.""" from datetime import datetime +from unittest.mock import patch import pytest @@ -24,7 +25,6 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util -from tests.async_mock import patch from tests.components.wled import init_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/wled/test_switch.py b/tests/components/wled/test_switch.py index 388e3317b39..c6e30ef903e 100644 --- a/tests/components/wled/test_switch.py +++ b/tests/components/wled/test_switch.py @@ -1,4 +1,6 @@ """Tests for the WLED switch platform.""" +from unittest.mock import patch + from wled import WLEDConnectionError from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN @@ -19,7 +21,6 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant -from tests.async_mock import patch from tests.components.wled import init_integration from tests.test_util.aiohttp import AiohttpClientMocker diff --git a/tests/components/wolflink/test_config_flow.py b/tests/components/wolflink/test_config_flow.py index 897650b48e8..5108883ed81 100644 --- a/tests/components/wolflink/test_config_flow.py +++ b/tests/components/wolflink/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Wolf SmartSet Service config flow.""" +from unittest.mock import patch + from httpcore import ConnectError from wolf_smartset.models import Device from wolf_smartset.token_auth import InvalidAuth @@ -12,7 +14,6 @@ from homeassistant.components.wolflink.const import ( ) from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from tests.async_mock import patch from tests.common import MockConfigEntry CONFIG = { diff --git a/tests/components/workday/test_binary_sensor.py b/tests/components/workday/test_binary_sensor.py index 7d50ec2994c..3ec17c3e6d3 100644 --- a/tests/components/workday/test_binary_sensor.py +++ b/tests/components/workday/test_binary_sensor.py @@ -1,5 +1,6 @@ """Tests the Home Assistant workday binary sensor.""" from datetime import date +from unittest.mock import patch import pytest import voluptuous as vol @@ -7,7 +8,6 @@ import voluptuous as vol import homeassistant.components.workday.binary_sensor as binary_sensor from homeassistant.setup import setup_component -from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant FUNCTION_PATH = "homeassistant.components.workday.binary_sensor.get_date" diff --git a/tests/components/xbox/test_config_flow.py b/tests/components/xbox/test_config_flow.py index 516a57c039b..7e2863a5861 100644 --- a/tests/components/xbox/test_config_flow.py +++ b/tests/components/xbox/test_config_flow.py @@ -1,9 +1,10 @@ """Test the xbox config flow.""" +from unittest.mock import patch + from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.xbox.const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN from homeassistant.helpers import config_entry_oauth2_flow -from tests.async_mock import patch from tests.common import MockConfigEntry CLIENT_ID = "1234" diff --git a/tests/components/xiaomi/test_device_tracker.py b/tests/components/xiaomi/test_device_tracker.py index 1760b3274a4..af5e192b9cf 100644 --- a/tests/components/xiaomi/test_device_tracker.py +++ b/tests/components/xiaomi/test_device_tracker.py @@ -1,5 +1,6 @@ """The tests for the Xiaomi router device tracker platform.""" import logging +from unittest.mock import MagicMock, call, patch import requests @@ -8,8 +9,6 @@ import homeassistant.components.xiaomi.device_tracker as xiaomi from homeassistant.components.xiaomi.device_tracker import get_scanner from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PLATFORM, CONF_USERNAME -from tests.async_mock import MagicMock, call, patch - _LOGGER = logging.getLogger(__name__) INVALID_USERNAME = "bob" diff --git a/tests/components/xiaomi_aqara/test_config_flow.py b/tests/components/xiaomi_aqara/test_config_flow.py index c84758f8b63..280775a7130 100644 --- a/tests/components/xiaomi_aqara/test_config_flow.py +++ b/tests/components/xiaomi_aqara/test_config_flow.py @@ -1,5 +1,6 @@ """Test the Xiaomi Aqara config flow.""" from socket import gaierror +from unittest.mock import Mock, patch import pytest @@ -8,8 +9,6 @@ from homeassistant.components import zeroconf from homeassistant.components.xiaomi_aqara import config_flow, const from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONF_PORT -from tests.async_mock import Mock, patch - ZEROCONF_NAME = "name" ZEROCONF_PROP = "properties" ZEROCONF_MAC = "mac" diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index d8ddd657efc..dbe78957586 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Xiaomi Miio config flow.""" +from unittest.mock import Mock, patch + from miio import DeviceException from homeassistant import config_entries @@ -6,8 +8,6 @@ from homeassistant.components import zeroconf from homeassistant.components.xiaomi_miio import config_flow, const from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN -from tests.async_mock import Mock, patch - ZEROCONF_NAME = "name" ZEROCONF_PROP = "properties" ZEROCONF_MAC = "mac" diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index 0fa241fb0b9..b1a3c08b84b 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -1,6 +1,7 @@ """The tests for the Xiaomi vacuum platform.""" from datetime import datetime, time, timedelta from unittest import mock +from unittest.mock import MagicMock, patch from miio import DeviceException import pytest @@ -57,8 +58,6 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, patch - PLATFORM = "xiaomi_miio" # calls made when device status is requested diff --git a/tests/components/yamaha/test_media_player.py b/tests/components/yamaha/test_media_player.py index 6a13c1d46e1..84a9e475c32 100644 --- a/tests/components/yamaha/test_media_player.py +++ b/tests/components/yamaha/test_media_player.py @@ -1,4 +1,6 @@ """The tests for the Yamaha Media player platform.""" +from unittest.mock import MagicMock, PropertyMock, call, patch + import pytest import homeassistant.components.media_player as mp @@ -7,8 +9,6 @@ from homeassistant.components.yamaha.const import DOMAIN from homeassistant.helpers.discovery import async_load_platform from homeassistant.setup import async_setup_component -from tests.async_mock import MagicMock, PropertyMock, call, patch - CONFIG = {"media_player": {"platform": "yamaha", "host": "127.0.0.1"}} diff --git a/tests/components/yandex_transport/test_yandex_transport_sensor.py b/tests/components/yandex_transport/test_yandex_transport_sensor.py index c70ebf6806c..a727ea6e6cd 100644 --- a/tests/components/yandex_transport/test_yandex_transport_sensor.py +++ b/tests/components/yandex_transport/test_yandex_transport_sensor.py @@ -1,6 +1,7 @@ """Tests for the yandex transport platform.""" import json +from unittest.mock import AsyncMock, patch import pytest @@ -9,7 +10,6 @@ from homeassistant.const import CONF_NAME from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import AsyncMock, patch from tests.common import assert_setup_component, load_fixture REPLY = json.loads(load_fixture("yandex_transport_reply.json")) diff --git a/tests/components/yeelight/__init__.py b/tests/components/yeelight/__init__.py index 5405b69490b..38c28a9a900 100644 --- a/tests/components/yeelight/__init__.py +++ b/tests/components/yeelight/__init__.py @@ -1,4 +1,6 @@ """Tests for the Yeelight integration.""" +from unittest.mock import MagicMock, patch + from yeelight import BulbException, BulbType from yeelight.main import _MODEL_SPECS @@ -12,8 +14,6 @@ from homeassistant.components.yeelight import ( ) from homeassistant.const import CONF_DEVICES, CONF_ID, CONF_NAME -from tests.async_mock import MagicMock, patch - IP_ADDRESS = "192.168.1.239" MODEL = "color" ID = "0x000000000015243f" diff --git a/tests/components/yeelight/test_binary_sensor.py b/tests/components/yeelight/test_binary_sensor.py index 8b2ec835722..f716469fc9a 100644 --- a/tests/components/yeelight/test_binary_sensor.py +++ b/tests/components/yeelight/test_binary_sensor.py @@ -1,4 +1,6 @@ """Test the Yeelight binary sensor.""" +from unittest.mock import patch + from homeassistant.components.yeelight import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_component @@ -6,8 +8,6 @@ from homeassistant.setup import async_setup_component from . import MODULE, NAME, PROPERTIES, YAML_CONFIGURATION, _mocked_bulb -from tests.async_mock import patch - ENTITY_BINARY_SENSOR = f"binary_sensor.{NAME}_nightlight" diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index 10191a5f6c7..8fa1ba5c988 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -1,4 +1,6 @@ """Test the Yeelight config flow.""" +from unittest.mock import MagicMock, patch + from homeassistant import config_entries from homeassistant.components.yeelight import ( CONF_DEVICE, @@ -30,7 +32,6 @@ from . import ( _patch_discovery, ) -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry DEFAULT_CONFIG = { diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index 882f9944ca1..c91ae33d986 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -1,5 +1,5 @@ """Test Yeelight.""" -from unittest.mock import MagicMock +from unittest.mock import MagicMock, patch from yeelight import BulbType @@ -32,7 +32,6 @@ from . import ( _patch_discovery, ) -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index 686ba6d8e82..0b7415140a3 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -1,5 +1,6 @@ """Test the Yeelight light.""" import logging +from unittest.mock import MagicMock, patch from yeelight import ( BulbException, @@ -31,6 +32,7 @@ from homeassistant.components.light import ( ) from homeassistant.components.yeelight import ( ATTR_COUNT, + ATTR_MODE_MUSIC, ATTR_TRANSITIONS, CONF_CUSTOM_EFFECTS, CONF_FLOW_PARAMS, @@ -72,6 +74,7 @@ from homeassistant.components.yeelight.light import ( SERVICE_SET_COLOR_TEMP_SCENE, SERVICE_SET_HSV_SCENE, SERVICE_SET_MODE, + SERVICE_SET_MUSIC_MODE, SERVICE_START_FLOW, SUPPORT_YEELIGHT, SUPPORT_YEELIGHT_RGB, @@ -105,7 +108,6 @@ from . import ( _patch_discovery, ) -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry CONFIG_ENTRY_DATA = { @@ -135,7 +137,14 @@ async def test_services(hass: HomeAssistant, caplog): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - async def _async_test_service(service, data, method, payload=None, domain=DOMAIN): + async def _async_test_service( + service, + data, + method, + payload=None, + domain=DOMAIN, + failure_side_effect=BulbException, + ): err_count = len([x for x in caplog.records if x.levelno == logging.ERROR]) # success @@ -153,13 +162,14 @@ async def test_services(hass: HomeAssistant, caplog): ) # failure - mocked_method = MagicMock(side_effect=BulbException) - setattr(type(mocked_bulb), method, mocked_method) - await hass.services.async_call(domain, service, data, blocking=True) - assert ( - len([x for x in caplog.records if x.levelno == logging.ERROR]) - == err_count + 1 - ) + if failure_side_effect: + mocked_method = MagicMock(side_effect=failure_side_effect) + setattr(type(mocked_bulb), method, mocked_method) + await hass.services.async_call(domain, service, data, blocking=True) + assert ( + len([x for x in caplog.records if x.levelno == logging.ERROR]) + == err_count + 1 + ) # turn_on brightness = 100 @@ -283,6 +293,29 @@ async def test_services(hass: HomeAssistant, caplog): [SceneClass.AUTO_DELAY_OFF, 50, 1], ) + # set_music_mode failure enable + await _async_test_service( + SERVICE_SET_MUSIC_MODE, + {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_MODE_MUSIC: "true"}, + "start_music", + failure_side_effect=AssertionError, + ) + + # set_music_mode disable + await _async_test_service( + SERVICE_SET_MUSIC_MODE, + {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_MODE_MUSIC: "false"}, + "stop_music", + failure_side_effect=None, + ) + + # set_music_mode success enable + await _async_test_service( + SERVICE_SET_MUSIC_MODE, + {ATTR_ENTITY_ID: ENTITY_LIGHT, ATTR_MODE_MUSIC: "true"}, + "start_music", + failure_side_effect=None, + ) # test _cmd wrapper error handler err_count = len([x for x in caplog.records if x.levelno == logging.ERROR]) type(mocked_bulb).turn_on = MagicMock() @@ -338,6 +371,7 @@ async def test_device_types(hass: HomeAssistant): target_properties["friendly_name"] = name target_properties["flowing"] = False target_properties["night_light"] = True + target_properties["music_mode"] = False assert dict(state.attributes) == target_properties await hass.config_entries.async_unload(config_entry.entry_id) @@ -365,6 +399,7 @@ async def test_device_types(hass: HomeAssistant): nightlight_properties["icon"] = "mdi:weather-night" nightlight_properties["flowing"] = False nightlight_properties["night_light"] = True + nightlight_properties["music_mode"] = False assert dict(state.attributes) == nightlight_properties await hass.config_entries.async_unload(config_entry.entry_id) diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 6b79c552911..cc34a511573 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -1,4 +1,6 @@ """Test Zeroconf component setup process.""" +from unittest.mock import patch + from zeroconf import ( BadTypeInNameException, InterfaceChoice, @@ -17,8 +19,6 @@ from homeassistant.const import ( from homeassistant.generated import zeroconf as zc_gen from homeassistant.setup import async_setup_component -from tests.async_mock import patch - NON_UTF8_VALUE = b"ABCDEF\x8a" NON_ASCII_KEY = b"non-ascii-key\x8a" PROPERTIES = { @@ -31,9 +31,11 @@ HOMEKIT_STATUS_UNPAIRED = b"1" HOMEKIT_STATUS_PAIRED = b"0" -def service_update_mock(zeroconf, services, handlers): +def service_update_mock(zeroconf, services, handlers, *, limit_service=None): """Call service update handler.""" for service in services: + if limit_service is not None and service != limit_service: + continue handlers[0](zeroconf, service, f"name.{service}", ServiceStateChange.Added) @@ -307,12 +309,16 @@ async def test_homekit_match_partial_space(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.dict( zc_gen.ZEROCONF, - {zeroconf.HOMEKIT_TYPE: [{"domain": "homekit_controller"}]}, + {"_hap._tcp.local.": [{"domain": "homekit_controller"}]}, clear=True, ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( - zeroconf, "HaServiceBrowser", side_effect=service_update_mock + zeroconf, + "HaServiceBrowser", + side_effect=lambda *args, **kwargs: service_update_mock( + *args, **kwargs, limit_service="_hap._tcp.local." + ), ) as mock_service_browser: mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( "LIFX bulb", HOMEKIT_STATUS_UNPAIRED @@ -330,12 +336,16 @@ async def test_homekit_match_partial_dash(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.dict( zc_gen.ZEROCONF, - {zeroconf.HOMEKIT_TYPE: [{"domain": "homekit_controller"}]}, + {"_hap._udp.local.": [{"domain": "homekit_controller"}]}, clear=True, ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( - zeroconf, "HaServiceBrowser", side_effect=service_update_mock + zeroconf, + "HaServiceBrowser", + side_effect=lambda *args, **kwargs: service_update_mock( + *args, **kwargs, limit_service="_hap._udp.local." + ), ) as mock_service_browser: mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( "Rachio-fa46ba", HOMEKIT_STATUS_UNPAIRED @@ -353,12 +363,16 @@ async def test_homekit_match_full(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.dict( zc_gen.ZEROCONF, - {zeroconf.HOMEKIT_TYPE: [{"domain": "homekit_controller"}]}, + {"_hap._udp.local.": [{"domain": "homekit_controller"}]}, clear=True, ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( - zeroconf, "HaServiceBrowser", side_effect=service_update_mock + zeroconf, + "HaServiceBrowser", + side_effect=lambda *args, **kwargs: service_update_mock( + *args, **kwargs, limit_service="_hap._udp.local." + ), ) as mock_service_browser: mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( "BSB002", HOMEKIT_STATUS_UNPAIRED @@ -376,12 +390,16 @@ async def test_homekit_already_paired(hass, mock_zeroconf): """Test that an already paired device is sent to homekit_controller.""" with patch.dict( zc_gen.ZEROCONF, - {zeroconf.HOMEKIT_TYPE: [{"domain": "homekit_controller"}]}, + {"_hap._tcp.local.": [{"domain": "homekit_controller"}]}, clear=True, ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( - zeroconf, "HaServiceBrowser", side_effect=service_update_mock + zeroconf, + "HaServiceBrowser", + side_effect=lambda *args, **kwargs: service_update_mock( + *args, **kwargs, limit_service="_hap._tcp.local." + ), ) as mock_service_browser: mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( "tado", HOMEKIT_STATUS_PAIRED @@ -400,12 +418,16 @@ async def test_homekit_invalid_paring_status(hass, mock_zeroconf): """Test that missing paring data is not sent to homekit_controller.""" with patch.dict( zc_gen.ZEROCONF, - {zeroconf.HOMEKIT_TYPE: [{"domain": "homekit_controller"}]}, + {"_hap._tcp.local.": [{"domain": "homekit_controller"}]}, clear=True, ), patch.object( hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( - zeroconf, "HaServiceBrowser", side_effect=service_update_mock + zeroconf, + "HaServiceBrowser", + side_effect=lambda *args, **kwargs: service_update_mock( + *args, **kwargs, limit_service="_hap._tcp.local." + ), ) as mock_service_browser: mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( "tado", b"invalid" @@ -423,7 +445,7 @@ async def test_homekit_not_paired(hass, mock_zeroconf): """Test that an not paired device is sent to homekit_controller.""" with patch.dict( zc_gen.ZEROCONF, - {zeroconf.HOMEKIT_TYPE: [{"domain": "homekit_controller"}]}, + {"_hap._tcp.local.": [{"domain": "homekit_controller"}]}, clear=True, ), patch.object( hass.config_entries.flow, "async_init" diff --git a/tests/components/zeroconf/test_usage.py b/tests/components/zeroconf/test_usage.py index 9cf953f4c8d..0f902632db8 100644 --- a/tests/components/zeroconf/test_usage.py +++ b/tests/components/zeroconf/test_usage.py @@ -1,12 +1,12 @@ """Test Zeroconf multiple instance protection.""" +from unittest.mock import Mock, patch + import zeroconf from homeassistant.components.zeroconf import async_get_instance from homeassistant.components.zeroconf.usage import install_multiple_zeroconf_catcher from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch - DOMAIN = "zeroconf" diff --git a/tests/components/zerproc/test_config_flow.py b/tests/components/zerproc/test_config_flow.py index 1a607bb8c9c..53b90e0364e 100644 --- a/tests/components/zerproc/test_config_flow.py +++ b/tests/components/zerproc/test_config_flow.py @@ -1,11 +1,11 @@ """Test the zerproc config flow.""" +from unittest.mock import patch + import pyzerproc from homeassistant import config_entries, setup from homeassistant.components.zerproc.config_flow import DOMAIN -from tests.async_mock import patch - async def test_flow_success(hass): """Test we get the form.""" diff --git a/tests/components/zerproc/test_light.py b/tests/components/zerproc/test_light.py index 77fdfb7d48a..1f0c7652bfd 100644 --- a/tests/components/zerproc/test_light.py +++ b/tests/components/zerproc/test_light.py @@ -1,4 +1,6 @@ """Test the zerproc lights.""" +from unittest.mock import MagicMock, patch + import pytest import pyzerproc @@ -24,7 +26,6 @@ from homeassistant.const import ( ) import homeassistant.util.dt as dt_util -from tests.async_mock import MagicMock, patch from tests.common import MockConfigEntry, async_fire_time_changed diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 249e1bf58b2..234ca0c9ba5 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -1,5 +1,6 @@ """Common test objects.""" import time +from unittest.mock import AsyncMock, Mock from zigpy.device import Device as zigpy_dev from zigpy.endpoint import Endpoint as zigpy_ep @@ -13,8 +14,6 @@ import zigpy.zdo.types import homeassistant.components.zha.core.const as zha_const from homeassistant.util import slugify -from tests.async_mock import AsyncMock, Mock - class FakeEndpoint: """Fake endpoint for moking zigpy.""" diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index 2a2ea6a1bb0..57241b9bb74 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -1,4 +1,6 @@ """Test configuration for the ZHA component.""" +from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch + import pytest import zigpy from zigpy.application import ControllerApplication @@ -13,7 +15,6 @@ from homeassistant.setup import async_setup_component from .common import FakeDevice, FakeEndpoint, get_zha_gateway -from tests.async_mock import AsyncMock, MagicMock, PropertyMock, patch from tests.common import MockConfigEntry from tests.components.light.conftest import mock_light_profiles # noqa diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py index 53861bc0d9a..363aa12db6e 100644 --- a/tests/components/zha/test_api.py +++ b/tests/components/zha/test_api.py @@ -1,5 +1,6 @@ """Test ZHA API.""" from binascii import unhexlify +from unittest.mock import AsyncMock, patch import pytest import voluptuous as vol @@ -41,8 +42,6 @@ from homeassistant.core import Context from .conftest import FIXTURE_GRP_ID, FIXTURE_GRP_NAME -from tests.async_mock import AsyncMock, patch - IEEE_SWITCH_DEVICE = "01:2d:6f:00:0a:90:69:e7" IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index e0c31d38bbb..8a2ca1f05c3 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -1,6 +1,7 @@ """Test ZHA Core channels.""" import asyncio from unittest import mock +from unittest.mock import AsyncMock, patch import pytest import zigpy.profiles.zha @@ -14,7 +15,6 @@ import homeassistant.components.zha.core.registries as registries from .common import get_zha_gateway, make_zcl_header -from tests.async_mock import AsyncMock, patch from tests.common import async_capture_events diff --git a/tests/components/zha/test_climate.py b/tests/components/zha/test_climate.py index cc152c1a36d..05412ddb64d 100644 --- a/tests/components/zha/test_climate.py +++ b/tests/components/zha/test_climate.py @@ -1,5 +1,7 @@ """Test zha climate.""" +from unittest.mock import patch + import pytest import zigpy.zcl.clusters from zigpy.zcl.clusters.hvac import Thermostat @@ -46,8 +48,6 @@ from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, STATE_UNKNOWN from .common import async_enable_traffic, find_entity_id, send_attributes_report -from tests.async_mock import patch - CLIMATE = { 1: { "device_type": zigpy.profiles.zha.DeviceType.THERMOSTAT, diff --git a/tests/components/zha/test_config_flow.py b/tests/components/zha/test_config_flow.py index 6fcc369182d..fe65def839d 100644 --- a/tests/components/zha/test_config_flow.py +++ b/tests/components/zha/test_config_flow.py @@ -1,6 +1,7 @@ """Tests for ZHA config flow.""" import os +from unittest.mock import AsyncMock, MagicMock, patch, sentinel import pytest import serial.tools.list_ports @@ -13,7 +14,6 @@ from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_SOURCE from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM -from tests.async_mock import AsyncMock, MagicMock, patch, sentinel from tests.common import MockConfigEntry diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py index 97fa5c7579d..c926618813c 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -1,5 +1,6 @@ """Test zha cover.""" import asyncio +from unittest.mock import AsyncMock, patch import pytest import zigpy.profiles.zha @@ -32,7 +33,6 @@ from .common import ( send_attributes_report, ) -from tests.async_mock import AsyncMock, patch from tests.common import async_capture_events, mock_coro, mock_restore_cache diff --git a/tests/components/zha/test_device.py b/tests/components/zha/test_device.py index 1cc9fb27d89..1ce75045d38 100644 --- a/tests/components/zha/test_device.py +++ b/tests/components/zha/test_device.py @@ -2,6 +2,7 @@ from datetime import timedelta import time from unittest import mock +from unittest.mock import patch import pytest import zigpy.profiles.zha @@ -14,7 +15,6 @@ import homeassistant.util.dt as dt_util from .common import async_enable_traffic, make_zcl_header -from tests.async_mock import patch from tests.common import async_fire_time_changed diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index c0350ce63a5..316d475f17f 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -50,7 +50,7 @@ async def test_get_actions(hass, device_ias): ieee_address = str(device_ias[0].ieee) ha_device_registry = await async_get_registry(hass) - reg_device = ha_device_registry.async_get_device({(DOMAIN, ieee_address)}, set()) + reg_device = ha_device_registry.async_get_device({(DOMAIN, ieee_address)}) actions = await async_get_device_automations(hass, "action", reg_device.id) @@ -73,7 +73,7 @@ async def test_action(hass, device_ias): ieee_address = str(zha_device.ieee) ha_device_registry = await async_get_registry(hass) - reg_device = ha_device_registry.async_get_device({(DOMAIN, ieee_address)}, set()) + reg_device = ha_device_registry.async_get_device({(DOMAIN, ieee_address)}) with patch( "zigpy.zcl.Cluster.request", diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index b72f693e531..96ee5520e2a 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -87,7 +87,7 @@ async def test_triggers(hass, mock_devices): ieee_address = str(zha_device.ieee) ha_device_registry = await async_get_registry(hass) - reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) + reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}) triggers = await async_get_device_automations(hass, "trigger", reg_device.id) @@ -145,7 +145,7 @@ async def test_no_triggers(hass, mock_devices): ieee_address = str(zha_device.ieee) ha_device_registry = await async_get_registry(hass) - reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) + reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}) triggers = await async_get_device_automations(hass, "trigger", reg_device.id) assert triggers == [ @@ -174,7 +174,7 @@ async def test_if_fires_on_event(hass, mock_devices, calls): ieee_address = str(zha_device.ieee) ha_device_registry = await async_get_registry(hass) - reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) + reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}) assert await async_setup_component( hass, @@ -283,7 +283,7 @@ async def test_exception_no_triggers(hass, mock_devices, calls, caplog): ieee_address = str(zha_device.ieee) ha_device_registry = await async_get_registry(hass) - reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) + reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}) await async_setup_component( hass, @@ -325,7 +325,7 @@ async def test_exception_bad_trigger(hass, mock_devices, calls, caplog): ieee_address = str(zha_device.ieee) ha_device_registry = await async_get_registry(hass) - reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}, set()) + reg_device = ha_device_registry.async_get_device({("zha", ieee_address)}) await async_setup_component( hass, diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index b5da98dc01f..ac2ef085e14 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -2,6 +2,7 @@ import re from unittest import mock +from unittest.mock import AsyncMock, patch import pytest import zigpy.profiles.zha @@ -30,8 +31,6 @@ import homeassistant.helpers.entity_registry from .common import get_zha_gateway from .zha_devices_list import DEVICES -from tests.async_mock import AsyncMock, patch - NO_TAIL_ID = re.compile("_\\d$") diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index 65be13fd96c..61828c135bc 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -1,4 +1,6 @@ """Test zha fan.""" +from unittest.mock import AsyncMock, call, patch + import pytest import zigpy.profiles.zha as zha import zigpy.zcl.clusters.general as general @@ -37,8 +39,6 @@ from .common import ( send_attributes_report, ) -from tests.async_mock import AsyncMock, call, patch - IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e8" diff --git a/tests/components/zha/test_init.py b/tests/components/zha/test_init.py index f0b82231fa9..f259febd817 100644 --- a/tests/components/zha/test_init.py +++ b/tests/components/zha/test_init.py @@ -1,5 +1,7 @@ """Tests for ZHA integration init.""" +from unittest.mock import AsyncMock, patch + import pytest from zigpy.config import CONF_DEVICE, CONF_DEVICE_PATH @@ -12,7 +14,6 @@ from homeassistant.components.zha.core.const import ( from homeassistant.const import MAJOR_VERSION, MINOR_VERSION from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, patch from tests.common import MockConfigEntry DATA_RADIO_TYPE = "deconz" diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index ea1b8487b7c..0a9a492a148 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -1,5 +1,6 @@ """Test zha light.""" from datetime import timedelta +from unittest.mock import AsyncMock, MagicMock, call, patch, sentinel import pytest import zigpy.profiles.zha as zha @@ -23,7 +24,6 @@ from .common import ( send_attributes_report, ) -from tests.async_mock import AsyncMock, MagicMock, call, patch, sentinel from tests.common import async_fire_time_changed ON = 1 diff --git a/tests/components/zha/test_number.py b/tests/components/zha/test_number.py index 947bad37e01..1bb9aa947ff 100644 --- a/tests/components/zha/test_number.py +++ b/tests/components/zha/test_number.py @@ -1,4 +1,6 @@ """Test zha analog output.""" +from unittest.mock import call, patch + import pytest import zigpy.profiles.zha import zigpy.types @@ -16,7 +18,6 @@ from .common import ( send_attributes_report, ) -from tests.async_mock import call, patch from tests.common import mock_coro diff --git a/tests/components/zodiac/test_sensor.py b/tests/components/zodiac/test_sensor.py index b08352dd2c0..6c784d3998f 100644 --- a/tests/components/zodiac/test_sensor.py +++ b/tests/components/zodiac/test_sensor.py @@ -1,5 +1,6 @@ """The test for the zodiac sensor platform.""" from datetime import datetime +from unittest.mock import patch import pytest @@ -19,8 +20,6 @@ from homeassistant.components.zodiac.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch - DAY1 = datetime(2020, 11, 15, tzinfo=dt_util.UTC) DAY2 = datetime(2020, 4, 20, tzinfo=dt_util.UTC) DAY3 = datetime(2020, 4, 21, tzinfo=dt_util.UTC) diff --git a/tests/components/zone/test_init.py b/tests/components/zone/test_init.py index 994bd5e6dda..07fd83cbe77 100644 --- a/tests/components/zone/test_init.py +++ b/tests/components/zone/test_init.py @@ -1,4 +1,6 @@ """Test zone component.""" +from unittest.mock import patch + import pytest from homeassistant import setup @@ -15,7 +17,6 @@ from homeassistant.core import Context from homeassistant.exceptions import Unauthorized from homeassistant.helpers import entity_registry -from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/zwave/conftest.py b/tests/components/zwave/conftest.py index 5366f028328..13da12c67ff 100644 --- a/tests/components/zwave/conftest.py +++ b/tests/components/zwave/conftest.py @@ -1,9 +1,10 @@ """Fixtures for Z-Wave tests.""" +from unittest.mock import AsyncMock, MagicMock, patch + import pytest from homeassistant.components.zwave import const -from tests.async_mock import AsyncMock, MagicMock, patch from tests.components.light.conftest import mock_light_profiles # noqa from tests.mock.zwave import MockNetwork, MockNode, MockOption, MockValue diff --git a/tests/components/zwave/test_binary_sensor.py b/tests/components/zwave/test_binary_sensor.py index 815563d2a5e..731e413caf8 100644 --- a/tests/components/zwave/test_binary_sensor.py +++ b/tests/components/zwave/test_binary_sensor.py @@ -1,9 +1,9 @@ """Test Z-Wave binary sensors.""" import datetime +from unittest.mock import patch from homeassistant.components.zwave import binary_sensor, const -from tests.async_mock import patch from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed diff --git a/tests/components/zwave/test_cover.py b/tests/components/zwave/test_cover.py index cde0957e2b3..e8b784feefe 100644 --- a/tests/components/zwave/test_cover.py +++ b/tests/components/zwave/test_cover.py @@ -1,4 +1,6 @@ """Test Z-Wave cover devices.""" +from unittest.mock import MagicMock + from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN from homeassistant.components.zwave import ( CONF_INVERT_OPENCLOSE_BUTTONS, @@ -7,7 +9,6 @@ from homeassistant.components.zwave import ( cover, ) -from tests.async_mock import MagicMock from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 5f408518271..d70c3d631d5 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -2,6 +2,7 @@ import asyncio from collections import OrderedDict from datetime import datetime +from unittest.mock import MagicMock, patch import pytest from pytz import utc @@ -20,7 +21,6 @@ from homeassistant.const import ATTR_ENTITY_ID from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg from homeassistant.helpers.entity_registry import async_get_registry -from tests.async_mock import MagicMock, patch from tests.common import async_fire_time_changed, mock_registry from tests.mock.zwave import MockEntityValues, MockNetwork, MockNode, MockValue diff --git a/tests/components/zwave/test_light.py b/tests/components/zwave/test_light.py index 1b973294daf..9e943c54bb4 100644 --- a/tests/components/zwave/test_light.py +++ b/tests/components/zwave/test_light.py @@ -1,4 +1,6 @@ """Test Z-Wave lights.""" +from unittest.mock import MagicMock, patch + from homeassistant.components import zwave from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -14,7 +16,6 @@ from homeassistant.components.light import ( ) from homeassistant.components.zwave import const, light -from tests.async_mock import MagicMock, patch from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed diff --git a/tests/components/zwave/test_lock.py b/tests/components/zwave/test_lock.py index 2f82bcb2764..d5b6d0a0d27 100644 --- a/tests/components/zwave/test_lock.py +++ b/tests/components/zwave/test_lock.py @@ -1,8 +1,9 @@ """Test Z-Wave locks.""" +from unittest.mock import MagicMock, patch + from homeassistant import config_entries from homeassistant.components.zwave import const, lock -from tests.async_mock import MagicMock, patch from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed diff --git a/tests/components/zwave/test_node_entity.py b/tests/components/zwave/test_node_entity.py index 29c1126b5d1..ba77aabc923 100644 --- a/tests/components/zwave/test_node_entity.py +++ b/tests/components/zwave/test_node_entity.py @@ -1,8 +1,9 @@ """Test Z-Wave node entity.""" +from unittest.mock import MagicMock, patch + from homeassistant.components.zwave import const, node_entity from homeassistant.const import ATTR_ENTITY_ID -from tests.async_mock import MagicMock, patch import tests.mock.zwave as mock_zwave diff --git a/tests/components/zwave/test_switch.py b/tests/components/zwave/test_switch.py index b61c456ccb9..4293a4a23fd 100644 --- a/tests/components/zwave/test_switch.py +++ b/tests/components/zwave/test_switch.py @@ -1,7 +1,8 @@ """Test Z-Wave switches.""" +from unittest.mock import patch + from homeassistant.components.zwave import switch -from tests.async_mock import patch from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed diff --git a/tests/components/zwave/test_websocket_api.py b/tests/components/zwave/test_websocket_api.py index 25bc364a630..9727906709f 100644 --- a/tests/components/zwave/test_websocket_api.py +++ b/tests/components/zwave/test_websocket_api.py @@ -1,4 +1,6 @@ """Test Z-Wave Websocket API.""" +from unittest.mock import call, patch + from homeassistant.bootstrap import async_setup_component from homeassistant.components.zwave.const import ( CONF_AUTOHEAL, @@ -8,6 +10,8 @@ from homeassistant.components.zwave.const import ( ) from homeassistant.components.zwave.websocket_api import ID, TYPE +NETWORK_KEY = "0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST" + async def test_zwave_ws_api(hass, mock_openzwave, hass_ws_client): """Test Z-Wave websocket API.""" @@ -20,7 +24,7 @@ async def test_zwave_ws_api(hass, mock_openzwave, hass_ws_client): CONF_AUTOHEAL: False, CONF_USB_STICK_PATH: "/dev/zwave", CONF_POLLING_INTERVAL: 6000, - CONF_NETWORK_KEY: "0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST", + CONF_NETWORK_KEY: NETWORK_KEY, } }, ) @@ -38,12 +42,47 @@ async def test_zwave_ws_api(hass, mock_openzwave, hass_ws_client): assert not result[CONF_AUTOHEAL] assert result[CONF_POLLING_INTERVAL] == 6000 + +async def test_zwave_ozw_migration_api(hass, mock_openzwave, hass_ws_client): + """Test Z-Wave to OpenZWave websocket migration API.""" + + await async_setup_component( + hass, + "zwave", + { + "zwave": { + CONF_AUTOHEAL: False, + CONF_USB_STICK_PATH: "/dev/zwave", + CONF_POLLING_INTERVAL: 6000, + CONF_NETWORK_KEY: NETWORK_KEY, + } + }, + ) + + await hass.async_block_till_done() + + client = await hass_ws_client(hass) + await client.send_json({ID: 6, TYPE: "zwave/get_migration_config"}) msg = await client.receive_json() result = msg["result"] assert result[CONF_USB_STICK_PATH] == "/dev/zwave" - assert ( - result[CONF_NETWORK_KEY] - == "0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST, 0xTE, 0xST" + assert result[CONF_NETWORK_KEY] == NETWORK_KEY + + with patch( + "homeassistant.config_entries.ConfigEntriesFlowManager.async_init" + ) as async_init: + + async_init.return_value = {"flow_id": "mock_flow_id"} + await client.send_json({ID: 7, TYPE: "zwave/start_ozw_config_flow"}) + msg = await client.receive_json() + + result = msg["result"] + + assert result["flow_id"] == "mock_flow_id" + assert async_init.call_args == call( + "ozw", + context={"source": "import"}, + data={"usb_path": "/dev/zwave", "network_key": NETWORK_KEY}, ) diff --git a/tests/components/zwave_js/__init__.py b/tests/components/zwave_js/__init__.py new file mode 100644 index 00000000000..bd4b740c856 --- /dev/null +++ b/tests/components/zwave_js/__init__.py @@ -0,0 +1 @@ +"""Tests for the Z-Wave JS integration.""" diff --git a/tests/components/zwave_js/common.py b/tests/components/zwave_js/common.py new file mode 100644 index 00000000000..63ec9013fa3 --- /dev/null +++ b/tests/components/zwave_js/common.py @@ -0,0 +1,15 @@ +"""Provide common test tools for Z-Wave JS.""" +AIR_TEMPERATURE_SENSOR = "sensor.multisensor_6_air_temperature" +ENERGY_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed_2" +POWER_SENSOR = "sensor.smart_plug_with_two_usb_ports_value_electric_consumed" +SWITCH_ENTITY = "switch.smart_plug_with_two_usb_ports_current_value" +LOW_BATTERY_BINARY_SENSOR = "binary_sensor.multisensor_6_low_battery_level" +ENABLED_LEGACY_BINARY_SENSOR = "binary_sensor.z_wave_door_window_sensor_any" +DISABLED_LEGACY_BINARY_SENSOR = "binary_sensor.multisensor_6_any" +NOTIFICATION_MOTION_BINARY_SENSOR = ( + "binary_sensor.multisensor_6_home_security_motion_detection" +) +NOTIFICATION_MOTION_SENSOR = "sensor.multisensor_6_home_security_motion_sensor_status" +PROPERTY_DOOR_STATUS_BINARY_SENSOR = ( + "binary_sensor.august_smart_lock_pro_3rd_gen_the_current_status_of_the_door" +) diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py new file mode 100644 index 00000000000..9cb950ba6e7 --- /dev/null +++ b/tests/components/zwave_js/conftest.py @@ -0,0 +1,347 @@ +"""Provide common Z-Wave JS fixtures.""" +import asyncio +import json +from unittest.mock import DEFAULT, AsyncMock, patch + +import pytest +from zwave_js_server.event import Event +from zwave_js_server.model.driver import Driver +from zwave_js_server.model.node import Node +from zwave_js_server.version import VersionInfo + +from homeassistant.helpers.device_registry import ( + async_get_registry as async_get_device_registry, +) + +from tests.common import MockConfigEntry, load_fixture + + +@pytest.fixture(name="device_registry") +async def device_registry_fixture(hass): + """Return the device registry.""" + return await async_get_device_registry(hass) + + +@pytest.fixture(name="discovery_info") +def discovery_info_fixture(): + """Return the discovery info from the supervisor.""" + return DEFAULT + + +@pytest.fixture(name="discovery_info_side_effect") +def discovery_info_side_effect_fixture(): + """Return the discovery info from the supervisor.""" + return None + + +@pytest.fixture(name="get_addon_discovery_info") +def mock_get_addon_discovery_info(discovery_info, discovery_info_side_effect): + """Mock get add-on discovery info.""" + with patch( + "homeassistant.components.hassio.async_get_addon_discovery_info", + side_effect=discovery_info_side_effect, + return_value=discovery_info, + ) as get_addon_discovery_info: + yield get_addon_discovery_info + + +@pytest.fixture(name="controller_state", scope="session") +def controller_state_fixture(): + """Load the controller state fixture data.""" + return json.loads(load_fixture("zwave_js/controller_state.json")) + + +@pytest.fixture(name="version_state", scope="session") +def version_state_fixture(): + """Load the version state fixture data.""" + return { + "type": "version", + "driverVersion": "6.0.0-beta.0", + "serverVersion": "1.0.0", + "homeId": 1234567890, + } + + +@pytest.fixture(name="multisensor_6_state", scope="session") +def multisensor_6_state_fixture(): + """Load the multisensor 6 node state fixture data.""" + return json.loads(load_fixture("zwave_js/multisensor_6_state.json")) + + +@pytest.fixture(name="ecolink_door_sensor_state", scope="session") +def ecolink_door_sensor_state_fixture(): + """Load the Ecolink Door/Window Sensor node state fixture data.""" + return json.loads(load_fixture("zwave_js/ecolink_door_sensor_state.json")) + + +@pytest.fixture(name="hank_binary_switch_state", scope="session") +def binary_switch_state_fixture(): + """Load the hank binary switch node state fixture data.""" + return json.loads(load_fixture("zwave_js/hank_binary_switch_state.json")) + + +@pytest.fixture(name="bulb_6_multi_color_state", scope="session") +def bulb_6_multi_color_state_fixture(): + """Load the bulb 6 multi-color node state fixture data.""" + return json.loads(load_fixture("zwave_js/bulb_6_multi_color_state.json")) + + +@pytest.fixture(name="eaton_rf9640_dimmer_state", scope="session") +def eaton_rf9640_dimmer_state_fixture(): + """Load the eaton rf9640 dimmer node state fixture data.""" + return json.loads(load_fixture("zwave_js/eaton_rf9640_dimmer_state.json")) + + +@pytest.fixture(name="lock_schlage_be469_state", scope="session") +def lock_schlage_be469_state_fixture(): + """Load the schlage lock node state fixture data.""" + return json.loads(load_fixture("zwave_js/lock_schlage_be469_state.json")) + + +@pytest.fixture(name="lock_august_asl03_state", scope="session") +def lock_august_asl03_state_fixture(): + """Load the August Pro lock node state fixture data.""" + return json.loads(load_fixture("zwave_js/lock_august_asl03_state.json")) + + +@pytest.fixture(name="climate_radio_thermostat_ct100_plus_state", scope="session") +def climate_radio_thermostat_ct100_plus_state_fixture(): + """Load the climate radio thermostat ct100 plus node state fixture data.""" + return json.loads( + load_fixture("zwave_js/climate_radio_thermostat_ct100_plus_state.json") + ) + + +@pytest.fixture( + name="climate_radio_thermostat_ct100_plus_different_endpoints_state", + scope="session", +) +def climate_radio_thermostat_ct100_plus_different_endpoints_state_fixture(): + """Load the thermostat fixture state with values on different endpoints. + + This device is a radio thermostat ct100. + """ + return json.loads( + load_fixture( + "zwave_js/climate_radio_thermostat_ct100_plus_different_endpoints_state.json" + ) + ) + + +@pytest.fixture(name="climate_danfoss_lc_13_state", scope="session") +def climate_danfoss_lc_13_state_fixture(): + """Load the climate Danfoss (LC-13) electronic radiator thermostat node state fixture data.""" + return json.loads(load_fixture("zwave_js/climate_danfoss_lc_13_state.json")) + + +@pytest.fixture(name="climate_heatit_z_trm3_state", scope="session") +def climate_heatit_z_trm3_state_fixture(): + """Load the climate HEATIT Z-TRM3 thermostat node state fixture data.""" + return json.loads(load_fixture("zwave_js/climate_heatit_z_trm3_state.json")) + + +@pytest.fixture(name="nortek_thermostat_state", scope="session") +def nortek_thermostat_state_fixture(): + """Load the nortek thermostat node state fixture data.""" + return json.loads(load_fixture("zwave_js/nortek_thermostat_state.json")) + + +@pytest.fixture(name="chain_actuator_zws12_state", scope="session") +def window_cover_state_fixture(): + """Load the window cover node state fixture data.""" + return json.loads(load_fixture("zwave_js/chain_actuator_zws12_state.json")) + + +@pytest.fixture(name="in_wall_smart_fan_control_state", scope="session") +def in_wall_smart_fan_control_state_fixture(): + """Load the fan node state fixture data.""" + return json.loads(load_fixture("zwave_js/in_wall_smart_fan_control_state.json")) + + +@pytest.fixture(name="client") +def mock_client_fixture(controller_state, version_state): + """Mock a client.""" + + with patch( + "homeassistant.components.zwave_js.ZwaveClient", autospec=True + ) as client_class: + client = client_class.return_value + + async def connect(): + await asyncio.sleep(0) + client.connected = True + + async def listen(driver_ready: asyncio.Event) -> None: + driver_ready.set() + await asyncio.sleep(30) + assert False, "Listen wasn't canceled!" + + async def disconnect(): + client.connected = False + + client.connect = AsyncMock(side_effect=connect) + client.listen = AsyncMock(side_effect=listen) + client.disconnect = AsyncMock(side_effect=disconnect) + client.driver = Driver(client, controller_state) + + client.version = VersionInfo.from_message(version_state) + client.ws_server_url = "ws://test:3000/zjs" + + yield client + + +@pytest.fixture(name="multisensor_6") +def multisensor_6_fixture(client, multisensor_6_state): + """Mock a multisensor 6 node.""" + node = Node(client, multisensor_6_state) + client.driver.controller.nodes[node.node_id] = node + return node + + +@pytest.fixture(name="ecolink_door_sensor") +def legacy_binary_sensor_fixture(client, ecolink_door_sensor_state): + """Mock a legacy_binary_sensor node.""" + node = Node(client, ecolink_door_sensor_state) + client.driver.controller.nodes[node.node_id] = node + return node + + +@pytest.fixture(name="hank_binary_switch") +def hank_binary_switch_fixture(client, hank_binary_switch_state): + """Mock a binary switch node.""" + node = Node(client, hank_binary_switch_state) + client.driver.controller.nodes[node.node_id] = node + return node + + +@pytest.fixture(name="bulb_6_multi_color") +def bulb_6_multi_color_fixture(client, bulb_6_multi_color_state): + """Mock a bulb 6 multi-color node.""" + node = Node(client, bulb_6_multi_color_state) + client.driver.controller.nodes[node.node_id] = node + return node + + +@pytest.fixture(name="eaton_rf9640_dimmer") +def eaton_rf9640_dimmer_fixture(client, eaton_rf9640_dimmer_state): + """Mock a Eaton RF9640 (V4 compatible) dimmer node.""" + node = Node(client, eaton_rf9640_dimmer_state) + client.driver.controller.nodes[node.node_id] = node + return node + + +@pytest.fixture(name="lock_schlage_be469") +def lock_schlage_be469_fixture(client, lock_schlage_be469_state): + """Mock a schlage lock node.""" + node = Node(client, lock_schlage_be469_state) + client.driver.controller.nodes[node.node_id] = node + return node + + +@pytest.fixture(name="lock_august_pro") +def lock_august_asl03_fixture(client, lock_august_asl03_state): + """Mock a August Pro lock node.""" + node = Node(client, lock_august_asl03_state) + client.driver.controller.nodes[node.node_id] = node + return node + + +@pytest.fixture(name="climate_radio_thermostat_ct100_plus") +def climate_radio_thermostat_ct100_plus_fixture( + client, climate_radio_thermostat_ct100_plus_state +): + """Mock a climate radio thermostat ct100 plus node.""" + node = Node(client, climate_radio_thermostat_ct100_plus_state) + client.driver.controller.nodes[node.node_id] = node + return node + + +@pytest.fixture(name="climate_radio_thermostat_ct100_plus_different_endpoints") +def climate_radio_thermostat_ct100_plus_different_endpoints_fixture( + client, climate_radio_thermostat_ct100_plus_different_endpoints_state +): + """Mock a climate radio thermostat ct100 plus node with values on different endpoints.""" + node = Node(client, climate_radio_thermostat_ct100_plus_different_endpoints_state) + client.driver.controller.nodes[node.node_id] = node + return node + + +@pytest.fixture(name="climate_danfoss_lc_13") +def climate_danfoss_lc_13_fixture(client, climate_danfoss_lc_13_state): + """Mock a climate radio danfoss LC-13 node.""" + node = Node(client, climate_danfoss_lc_13_state) + client.driver.controller.nodes[node.node_id] = node + return node + + +@pytest.fixture(name="climate_heatit_z_trm3") +def climate_heatit_z_trm3_fixture(client, climate_heatit_z_trm3_state): + """Mock a climate radio HEATIT Z-TRM3 node.""" + node = Node(client, climate_heatit_z_trm3_state) + client.driver.controller.nodes[node.node_id] = node + return node + + +@pytest.fixture(name="nortek_thermostat") +def nortek_thermostat_fixture(client, nortek_thermostat_state): + """Mock a nortek thermostat node.""" + node = Node(client, nortek_thermostat_state) + client.driver.controller.nodes[node.node_id] = node + return node + + +@pytest.fixture(name="nortek_thermostat_added_event") +def nortek_thermostat_added_event_fixture(client): + """Mock a Nortek thermostat node added event.""" + event_data = json.loads(load_fixture("zwave_js/nortek_thermostat_added_event.json")) + event = Event("node added", event_data) + return event + + +@pytest.fixture(name="nortek_thermostat_removed_event") +def nortek_thermostat_removed_event_fixture(client): + """Mock a Nortek thermostat node removed event.""" + event_data = json.loads( + load_fixture("zwave_js/nortek_thermostat_removed_event.json") + ) + event = Event("node removed", event_data) + return event + + +@pytest.fixture(name="integration") +async def integration_fixture(hass, client): + """Set up the zwave_js integration.""" + entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"}) + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry + + +@pytest.fixture(name="chain_actuator_zws12") +def window_cover_fixture(client, chain_actuator_zws12_state): + """Mock a window cover node.""" + node = Node(client, chain_actuator_zws12_state) + client.driver.controller.nodes[node.node_id] = node + return node + + +@pytest.fixture(name="in_wall_smart_fan_control") +def in_wall_smart_fan_control_fixture(client, in_wall_smart_fan_control_state): + """Mock a fan node.""" + node = Node(client, in_wall_smart_fan_control_state) + client.driver.controller.nodes[node.node_id] = node + return node + + +@pytest.fixture(name="multiple_devices") +def multiple_devices_fixture( + client, climate_radio_thermostat_ct100_plus_state, lock_schlage_be469_state +): + """Mock a client with multiple devices.""" + node = Node(client, climate_radio_thermostat_ct100_plus_state) + client.driver.controller.nodes[node.node_id] = node + node = Node(client, lock_schlage_be469_state) + client.driver.controller.nodes[node.node_id] = node + return client.driver.controller.nodes diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py new file mode 100644 index 00000000000..88e8acc5771 --- /dev/null +++ b/tests/components/zwave_js/test_api.py @@ -0,0 +1,174 @@ +"""Test the Z-Wave JS Websocket API.""" +from unittest.mock import patch + +from zwave_js_server.event import Event + +from homeassistant.components.zwave_js.api import ENTRY_ID, ID, NODE_ID, TYPE +from homeassistant.components.zwave_js.const import DOMAIN +from homeassistant.helpers.device_registry import async_get_registry + + +async def test_websocket_api(hass, integration, multisensor_6, hass_ws_client): + """Test the network and node status websocket commands.""" + entry = integration + ws_client = await hass_ws_client(hass) + + await ws_client.send_json( + {ID: 2, TYPE: "zwave_js/network_status", ENTRY_ID: entry.entry_id} + ) + msg = await ws_client.receive_json() + result = msg["result"] + + assert result["client"]["ws_server_url"] == "ws://test:3000/zjs" + assert result["client"]["server_version"] == "1.0.0" + + node = multisensor_6 + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/node_status", + ENTRY_ID: entry.entry_id, + NODE_ID: node.node_id, + } + ) + msg = await ws_client.receive_json() + result = msg["result"] + + assert result[NODE_ID] == 52 + assert result["ready"] + assert result["is_routing"] + assert not result["is_secure"] + assert result["status"] == 1 + + +async def test_add_node( + hass, integration, client, hass_ws_client, nortek_thermostat_added_event +): + """Test the add_node websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + client.async_send_command.return_value = {"success": True} + + await ws_client.send_json( + {ID: 3, TYPE: "zwave_js/add_node", ENTRY_ID: entry.entry_id} + ) + + msg = await ws_client.receive_json() + assert msg["success"] + + event = Event( + type="inclusion started", + data={ + "source": "controller", + "event": "inclusion started", + "secure": False, + }, + ) + client.driver.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "inclusion started" + + client.driver.receive_event(nortek_thermostat_added_event) + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "node added" + + +async def test_cancel_inclusion_exclusion(hass, integration, client, hass_ws_client): + """Test cancelling the inclusion and exclusion process.""" + entry = integration + ws_client = await hass_ws_client(hass) + + client.async_send_command.return_value = {"success": True} + + await ws_client.send_json( + {ID: 4, TYPE: "zwave_js/stop_inclusion", ENTRY_ID: entry.entry_id} + ) + + msg = await ws_client.receive_json() + assert msg["success"] + + await ws_client.send_json( + {ID: 5, TYPE: "zwave_js/stop_exclusion", ENTRY_ID: entry.entry_id} + ) + + msg = await ws_client.receive_json() + assert msg["success"] + + +async def test_remove_node( + hass, + integration, + client, + hass_ws_client, + nortek_thermostat, + nortek_thermostat_removed_event, +): + """Test the remove_node websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + client.async_send_command.return_value = {"success": True} + + await ws_client.send_json( + {ID: 3, TYPE: "zwave_js/remove_node", ENTRY_ID: entry.entry_id} + ) + + msg = await ws_client.receive_json() + assert msg["success"] + + event = Event( + type="exclusion started", + data={ + "source": "controller", + "event": "exclusion started", + "secure": False, + }, + ) + client.driver.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "exclusion started" + + # Add mock node to controller + client.driver.controller.nodes[67] = nortek_thermostat + + dev_reg = await async_get_registry(hass) + + # Create device registry entry for mock node + device = dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, "3245146787-67")}, + name="Node 67", + ) + + # Fire node removed event + client.driver.receive_event(nortek_thermostat_removed_event) + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "node removed" + + # Verify device was removed from device registry + device = dev_reg.async_get_device( + identifiers={(DOMAIN, "3245146787-67")}, + ) + assert device is None + + +async def test_dump_view(integration, hass_client): + """Test the HTTP dump view.""" + client = await hass_client() + with patch( + "zwave_js_server.dump.dump_msgs", + return_value=[{"hello": "world"}, {"second": "msg"}], + ): + resp = await client.get(f"/api/zwave_js/dump/{integration.entry_id}") + assert resp.status == 200 + assert await resp.text() == '{"hello": "world"}\n{"second": "msg"}\n' + + +async def test_dump_view_invalid_entry_id(integration, hass_client): + """Test an invalid config entry id parameter.""" + client = await hass_client() + resp = await client.get("/api/zwave_js/dump/INVALID") + assert resp.status == 400 diff --git a/tests/components/zwave_js/test_binary_sensor.py b/tests/components/zwave_js/test_binary_sensor.py new file mode 100644 index 00000000000..e8361d8b03e --- /dev/null +++ b/tests/components/zwave_js/test_binary_sensor.py @@ -0,0 +1,140 @@ +"""Test the Z-Wave JS binary sensor platform.""" +from zwave_js_server.event import Event + +from homeassistant.components.binary_sensor import DEVICE_CLASS_MOTION +from homeassistant.const import DEVICE_CLASS_BATTERY, STATE_OFF, STATE_ON + +from .common import ( + DISABLED_LEGACY_BINARY_SENSOR, + ENABLED_LEGACY_BINARY_SENSOR, + LOW_BATTERY_BINARY_SENSOR, + NOTIFICATION_MOTION_BINARY_SENSOR, + PROPERTY_DOOR_STATUS_BINARY_SENSOR, +) + + +async def test_low_battery_sensor(hass, multisensor_6, integration): + """Test boolean binary sensor of type low battery.""" + state = hass.states.get(LOW_BATTERY_BINARY_SENSOR) + + assert state + assert state.state == STATE_OFF + assert state.attributes["device_class"] == DEVICE_CLASS_BATTERY + + +async def test_enabled_legacy_sensor(hass, ecolink_door_sensor, integration): + """Test enabled legacy boolean binary sensor.""" + node = ecolink_door_sensor + # this node has Notification CC not (fully) implemented + # so legacy binary sensor should be enabled + + state = hass.states.get(ENABLED_LEGACY_BINARY_SENSOR) + assert state + assert state.state == STATE_OFF + assert state.attributes.get("device_class") is None + + # Test state updates from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 53, + "args": { + "commandClassName": "Binary Sensor", + "commandClass": 48, + "endpoint": 0, + "property": "Any", + "newValue": True, + "prevValue": False, + "propertyName": "Any", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(ENABLED_LEGACY_BINARY_SENSOR) + assert state.state == STATE_ON + + +async def test_disabled_legacy_sensor(hass, multisensor_6, integration): + """Test disabled legacy boolean binary sensor.""" + # this node has Notification CC implemented so legacy binary sensor should be disabled + + registry = await hass.helpers.entity_registry.async_get_registry() + entity_id = DISABLED_LEGACY_BINARY_SENSOR + state = hass.states.get(entity_id) + assert state is None + entry = registry.async_get(entity_id) + assert entry + assert entry.disabled + assert entry.disabled_by == "integration" + + # Test enabling legacy entity + updated_entry = registry.async_update_entity( + entry.entity_id, **{"disabled_by": None} + ) + assert updated_entry != entry + assert updated_entry.disabled is False + + +async def test_notification_sensor(hass, multisensor_6, integration): + """Test binary sensor created from Notification CC.""" + state = hass.states.get(NOTIFICATION_MOTION_BINARY_SENSOR) + + assert state + assert state.state == STATE_ON + assert state.attributes["device_class"] == DEVICE_CLASS_MOTION + + +async def test_property_sensor_door_status(hass, lock_august_pro, integration): + """Test property binary sensor with sensor mapping (doorStatus).""" + node = lock_august_pro + + state = hass.states.get(PROPERTY_DOOR_STATUS_BINARY_SENSOR) + assert state is not None + assert state.state == STATE_OFF + + # open door + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 6, + "args": { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "doorStatus", + "newValue": "open", + "prevValue": "closed", + "propertyName": "doorStatus", + }, + }, + ) + node.receive_event(event) + state = hass.states.get(PROPERTY_DOOR_STATUS_BINARY_SENSOR) + assert state.state == STATE_ON + + # close door + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 6, + "args": { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "doorStatus", + "newValue": "closed", + "prevValue": "open", + "propertyName": "doorStatus", + }, + }, + ) + node.receive_event(event) + state = hass.states.get(PROPERTY_DOOR_STATUS_BINARY_SENSOR) + assert state.state == STATE_OFF diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py new file mode 100644 index 00000000000..b2455f3cbbd --- /dev/null +++ b/tests/components/zwave_js/test_climate.py @@ -0,0 +1,433 @@ +"""Test the Z-Wave JS climate platform.""" +import pytest +from zwave_js_server.event import Event + +from homeassistant.components.climate.const import ( + ATTR_CURRENT_HUMIDITY, + ATTR_CURRENT_TEMPERATURE, + ATTR_HVAC_ACTION, + ATTR_HVAC_MODE, + ATTR_HVAC_MODES, + ATTR_PRESET_MODE, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + CURRENT_HVAC_IDLE, + DOMAIN as CLIMATE_DOMAIN, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + PRESET_NONE, + SERVICE_SET_HVAC_MODE, + SERVICE_SET_PRESET_MODE, + SERVICE_SET_TEMPERATURE, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE + +CLIMATE_RADIO_THERMOSTAT_ENTITY = "climate.z_wave_thermostat_thermostat_mode" +CLIMATE_DANFOSS_LC13_ENTITY = "climate.living_connect_z_thermostat_heating" +CLIMATE_FLOOR_THERMOSTAT_ENTITY = "climate.floor_thermostat_thermostat_mode" + + +async def test_thermostat_v2( + hass, client, climate_radio_thermostat_ct100_plus, integration +): + """Test a thermostat v2 command class entity.""" + node = climate_radio_thermostat_ct100_plus + state = hass.states.get(CLIMATE_RADIO_THERMOSTAT_ENTITY) + + assert state + assert state.state == HVAC_MODE_HEAT + assert state.attributes[ATTR_HVAC_MODES] == [ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + HVAC_MODE_HEAT_COOL, + ] + assert state.attributes[ATTR_CURRENT_HUMIDITY] == 30 + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.2 + assert state.attributes[ATTR_TEMPERATURE] == 22.2 + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE + + # Test setting preset mode + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + { + ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY, + ATTR_PRESET_MODE: PRESET_NONE, + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 13 + assert args["valueId"] == { + "commandClassName": "Thermostat Mode", + "commandClass": 64, + "endpoint": 1, + "property": "mode", + "propertyName": "mode", + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "min": 0, + "max": 31, + "label": "Thermostat mode", + "states": {"0": "Off", "1": "Heat", "2": "Cool", "3": "Auto"}, + }, + "value": 1, + } + assert args["value"] == 1 + + client.async_send_command.reset_mock() + + # Test setting hvac mode + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + { + ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY, + ATTR_HVAC_MODE: HVAC_MODE_COOL, + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 13 + assert args["valueId"] == { + "commandClassName": "Thermostat Mode", + "commandClass": 64, + "endpoint": 1, + "property": "mode", + "propertyName": "mode", + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "min": 0, + "max": 31, + "label": "Thermostat mode", + "states": {"0": "Off", "1": "Heat", "2": "Cool", "3": "Auto"}, + }, + "value": 1, + } + assert args["value"] == 2 + + client.async_send_command.reset_mock() + + # Test setting temperature + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY, + ATTR_HVAC_MODE: HVAC_MODE_COOL, + ATTR_TEMPERATURE: 25, + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 2 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 13 + assert args["valueId"] == { + "commandClassName": "Thermostat Mode", + "commandClass": 64, + "endpoint": 1, + "property": "mode", + "propertyName": "mode", + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "min": 0, + "max": 31, + "label": "Thermostat mode", + "states": {"0": "Off", "1": "Heat", "2": "Cool", "3": "Auto"}, + }, + "value": 1, + } + assert args["value"] == 2 + args = client.async_send_command.call_args_list[1][0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 13 + assert args["valueId"] == { + "commandClassName": "Thermostat Setpoint", + "commandClass": 67, + "endpoint": 1, + "property": "setpoint", + "propertyKey": 1, + "propertyName": "setpoint", + "propertyKeyName": "Heating", + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "unit": "°F", + "ccSpecific": {"setpointType": 1}, + }, + "value": 72, + } + assert args["value"] == 77 + + client.async_send_command.reset_mock() + + # Test cool mode update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 13, + "args": { + "commandClassName": "Thermostat Mode", + "commandClass": 64, + "endpoint": 1, + "property": "mode", + "propertyName": "mode", + "newValue": 2, + "prevValue": 1, + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(CLIMATE_RADIO_THERMOSTAT_ENTITY) + assert state.state == HVAC_MODE_COOL + assert state.attributes[ATTR_TEMPERATURE] == 22.8 + + # Test heat_cool mode update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 13, + "args": { + "commandClassName": "Thermostat Mode", + "commandClass": 64, + "endpoint": 1, + "property": "mode", + "propertyName": "mode", + "newValue": 3, + "prevValue": 1, + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(CLIMATE_RADIO_THERMOSTAT_ENTITY) + assert state.state == HVAC_MODE_HEAT_COOL + assert state.attributes[ATTR_TARGET_TEMP_HIGH] == 22.8 + assert state.attributes[ATTR_TARGET_TEMP_LOW] == 22.2 + + client.async_send_command.reset_mock() + + # Test setting temperature with heat_cool + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY, + ATTR_TARGET_TEMP_HIGH: 30, + ATTR_TARGET_TEMP_LOW: 25, + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 2 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 13 + assert args["valueId"] == { + "commandClassName": "Thermostat Setpoint", + "commandClass": 67, + "endpoint": 1, + "property": "setpoint", + "propertyKey": 1, + "propertyName": "setpoint", + "propertyKeyName": "Heating", + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "unit": "°F", + "ccSpecific": {"setpointType": 1}, + }, + "value": 72, + } + assert args["value"] == 77 + + args = client.async_send_command.call_args_list[1][0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 13 + assert args["valueId"] == { + "commandClassName": "Thermostat Setpoint", + "commandClass": 67, + "endpoint": 1, + "property": "setpoint", + "propertyKey": 2, + "propertyName": "setpoint", + "propertyKeyName": "Cooling", + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "unit": "°F", + "ccSpecific": {"setpointType": 2}, + }, + "value": 73, + } + assert args["value"] == 86 + + client.async_send_command.reset_mock() + + with pytest.raises(ValueError): + # Test setting unknown preset mode + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + { + ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY, + ATTR_PRESET_MODE: "unknown_preset", + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 0 + + # Test setting invalid hvac mode + with pytest.raises(ValueError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_HVAC_MODE, + { + ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY, + ATTR_HVAC_MODE: HVAC_MODE_DRY, + }, + blocking=True, + ) + + # Test setting invalid preset mode + with pytest.raises(ValueError): + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_PRESET_MODE, + { + ATTR_ENTITY_ID: CLIMATE_RADIO_THERMOSTAT_ENTITY, + ATTR_PRESET_MODE: "invalid_mode", + }, + blocking=True, + ) + + +async def test_thermostat_different_endpoints( + hass, client, climate_radio_thermostat_ct100_plus_different_endpoints, integration +): + """Test an entity with values on a different endpoint from the primary value.""" + state = hass.states.get(CLIMATE_RADIO_THERMOSTAT_ENTITY) + + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.5 + + +async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integration): + """Test a setpoint thermostat command class entity.""" + node = climate_danfoss_lc_13 + state = hass.states.get(CLIMATE_DANFOSS_LC13_ENTITY) + + assert state + assert state.state == HVAC_MODE_HEAT + assert state.attributes[ATTR_TEMPERATURE] == 25 + assert state.attributes[ATTR_HVAC_MODES] == [HVAC_MODE_HEAT] + assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE + + client.async_send_command.reset_mock() + + # Test setting temperature + await hass.services.async_call( + CLIMATE_DOMAIN, + SERVICE_SET_TEMPERATURE, + { + ATTR_ENTITY_ID: CLIMATE_DANFOSS_LC13_ENTITY, + ATTR_TEMPERATURE: 21.5, + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args_list[0][0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 5 + assert args["valueId"] == { + "endpoint": 0, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyName": "setpoint", + "propertyKeyName": "Heating", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "unit": "\u00b0C", + "ccSpecific": {"setpointType": 1}, + }, + "value": 25, + } + assert args["value"] == 21.5 + + client.async_send_command.reset_mock() + + # Test setpoint mode update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 5, + "args": { + "commandClassName": "Thermostat Setpoint", + "commandClass": 67, + "endpoint": 0, + "property": "setpoint", + "propertyKey": 1, + "propertyKeyName": "Heating", + "propertyName": "setpoint", + "newValue": 23, + "prevValue": 21.5, + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(CLIMATE_DANFOSS_LC13_ENTITY) + assert state.state == HVAC_MODE_HEAT + assert state.attributes[ATTR_TEMPERATURE] == 23 + + client.async_send_command.reset_mock() + + +async def test_thermostat_heatit(hass, client, climate_heatit_z_trm3, integration): + """Test a thermostat v2 command class entity.""" + state = hass.states.get(CLIMATE_FLOOR_THERMOSTAT_ENTITY) + + assert state + assert state.state == HVAC_MODE_HEAT + assert state.attributes[ATTR_HVAC_MODES] == [ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + ] + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 22.9 + assert state.attributes[ATTR_TEMPERATURE] == 22.5 + assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_IDLE + assert state.attributes[ATTR_PRESET_MODE] == PRESET_NONE diff --git a/tests/components/zwave_js/test_config_flow.py b/tests/components/zwave_js/test_config_flow.py new file mode 100644 index 00000000000..0270383174e --- /dev/null +++ b/tests/components/zwave_js/test_config_flow.py @@ -0,0 +1,880 @@ +"""Test the Z-Wave JS config flow.""" +import asyncio +from unittest.mock import patch + +import pytest +from zwave_js_server.version import VersionInfo + +from homeassistant import config_entries, setup +from homeassistant.components.hassio.handler import HassioAPIError +from homeassistant.components.zwave_js.config_flow import TITLE +from homeassistant.components.zwave_js.const import DOMAIN + +from tests.common import MockConfigEntry + +ADDON_DISCOVERY_INFO = { + "addon": "Z-Wave JS", + "host": "host1", + "port": 3001, +} + + +@pytest.fixture(name="supervisor") +def mock_supervisor_fixture(): + """Mock Supervisor.""" + with patch("homeassistant.components.hassio.is_hassio", return_value=True): + yield + + +@pytest.fixture(name="addon_info_side_effect") +def addon_info_side_effect_fixture(): + """Return the add-on info side effect.""" + return None + + +@pytest.fixture(name="addon_info") +def mock_addon_info(addon_info_side_effect): + """Mock Supervisor add-on info.""" + with patch( + "homeassistant.components.hassio.async_get_addon_info", + side_effect=addon_info_side_effect, + ) as addon_info: + addon_info.return_value = {} + yield addon_info + + +@pytest.fixture(name="addon_running") +def mock_addon_running(addon_info): + """Mock add-on already running.""" + addon_info.return_value["state"] = "started" + return addon_info + + +@pytest.fixture(name="addon_installed") +def mock_addon_installed(addon_info): + """Mock add-on already installed but not running.""" + addon_info.return_value["state"] = "stopped" + addon_info.return_value["version"] = "1.0" + return addon_info + + +@pytest.fixture(name="addon_options") +def mock_addon_options(addon_info): + """Mock add-on options.""" + addon_info.return_value["options"] = {} + return addon_info.return_value["options"] + + +@pytest.fixture(name="set_addon_options_side_effect") +def set_addon_options_side_effect_fixture(): + """Return the set add-on options side effect.""" + return None + + +@pytest.fixture(name="set_addon_options") +def mock_set_addon_options(set_addon_options_side_effect): + """Mock set add-on options.""" + with patch( + "homeassistant.components.hassio.async_set_addon_options", + side_effect=set_addon_options_side_effect, + ) as set_options: + yield set_options + + +@pytest.fixture(name="install_addon") +def mock_install_addon(): + """Mock install add-on.""" + with patch("homeassistant.components.hassio.async_install_addon") as install_addon: + yield install_addon + + +@pytest.fixture(name="start_addon_side_effect") +def start_addon_side_effect_fixture(): + """Return the set add-on options side effect.""" + return None + + +@pytest.fixture(name="start_addon") +def mock_start_addon(start_addon_side_effect): + """Mock start add-on.""" + with patch( + "homeassistant.components.hassio.async_start_addon", + side_effect=start_addon_side_effect, + ) as start_addon: + yield start_addon + + +@pytest.fixture(name="server_version_side_effect") +def server_version_side_effect_fixture(): + """Return the server version side effect.""" + return None + + +@pytest.fixture(name="get_server_version", autouse=True) +def mock_get_server_version(server_version_side_effect): + """Mock server version.""" + version_info = VersionInfo( + driver_version="mock-driver-version", + server_version="mock-server-version", + home_id=1234, + ) + with patch( + "homeassistant.components.zwave_js.config_flow.get_server_version", + side_effect=server_version_side_effect, + return_value=version_info, + ) as mock_version: + yield mock_version + + +@pytest.fixture(name="addon_setup_time", autouse=True) +def mock_addon_setup_time(): + """Mock add-on setup sleep time.""" + with patch( + "homeassistant.components.zwave_js.config_flow.ADDON_SETUP_TIME", new=0 + ) as addon_setup_time: + yield addon_setup_time + + +async def test_manual(hass): + """Test we create an entry with manual step.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + + with patch( + "homeassistant.components.zwave_js.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.zwave_js.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": "ws://localhost:3000", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "Z-Wave JS" + assert result2["data"] == { + "url": "ws://localhost:3000", + "usb_path": None, + "network_key": None, + "use_addon": False, + "integration_created_addon": False, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + assert result2["result"].unique_id == 1234 + + +@pytest.mark.parametrize( + "url, server_version_side_effect, error", + [ + ( + "not-ws-url", + None, + "invalid_ws_url", + ), + ( + "ws://localhost:3000", + asyncio.TimeoutError, + "cannot_connect", + ), + ( + "ws://localhost:3000", + Exception("Boom"), + "unknown", + ), + ], +) +async def test_manual_errors( + hass, + url, + error, +): + """Test all errors with a manual set up.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "manual" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": url, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "manual" + assert result["errors"] == {"base": error} + + +async def test_manual_already_configured(hass): + """Test that only one unique instance is allowed.""" + entry = MockConfigEntry(domain=DOMAIN, data={}, title=TITLE, unique_id=1234) + entry.add_to_hass(hass) + + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "manual" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": "ws://localhost:3000", + }, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) +async def test_supervisor_discovery( + hass, supervisor, addon_running, addon_options, get_addon_discovery_info +): + """Test flow started from Supervisor discovery.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + addon_options["device"] = "/test" + addon_options["network_key"] = "abc123" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=ADDON_DISCOVERY_INFO, + ) + + with patch( + "homeassistant.components.zwave_js.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.zwave_js.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == TITLE + assert result["data"] == { + "url": "ws://host1:3001", + "usb_path": "/test", + "network_key": "abc123", + "use_addon": True, + "integration_created_addon": False, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "discovery_info, server_version_side_effect", + [({"config": ADDON_DISCOVERY_INFO}, asyncio.TimeoutError())], +) +async def test_supervisor_discovery_cannot_connect( + hass, supervisor, get_addon_discovery_info +): + """Test Supervisor discovery and cannot connect.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=ADDON_DISCOVERY_INFO, + ) + + assert result["type"] == "abort" + assert result["reason"] == "cannot_connect" + + +@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) +async def test_clean_discovery_on_user_create( + hass, supervisor, addon_running, addon_options, get_addon_discovery_info +): + """Test discovery flow is cleaned up when a user flow is finished.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + addon_options["device"] = "/test" + addon_options["network_key"] = "abc123" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=ADDON_DISCOVERY_INFO, + ) + + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": False} + ) + + assert result["type"] == "form" + assert result["step_id"] == "manual" + + with patch( + "homeassistant.components.zwave_js.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.zwave_js.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": "ws://localhost:3000", + }, + ) + await hass.async_block_till_done() + + assert len(hass.config_entries.flow.async_progress()) == 0 + assert result["type"] == "create_entry" + assert result["title"] == TITLE + assert result["data"] == { + "url": "ws://localhost:3000", + "usb_path": None, + "network_key": None, + "use_addon": False, + "integration_created_addon": False, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_abort_discovery_with_existing_entry( + hass, supervisor, addon_running, addon_options +): + """Test discovery flow is aborted if an entry already exists.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + entry = MockConfigEntry( + domain=DOMAIN, data={"url": "ws://localhost:3000"}, title=TITLE, unique_id=1234 + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=ADDON_DISCOVERY_INFO, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + # Assert that the entry data is updated with discovery info. + assert entry.data["url"] == "ws://host1:3001" + + +async def test_discovery_addon_not_running( + hass, supervisor, addon_installed, addon_options, set_addon_options, start_addon +): + """Test discovery with add-on already installed but not running.""" + addon_options["device"] = None + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=ADDON_DISCOVERY_INFO, + ) + + assert result["step_id"] == "hassio_confirm" + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["step_id"] == "start_addon" + assert result["type"] == "form" + + +async def test_discovery_addon_not_installed( + hass, supervisor, addon_installed, install_addon, addon_options +): + """Test discovery with add-on not installed.""" + addon_installed.return_value["version"] = None + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HASSIO}, + data=ADDON_DISCOVERY_INFO, + ) + + assert result["step_id"] == "hassio_confirm" + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result["step_id"] == "install_addon" + assert result["type"] == "progress" + + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == "form" + assert result["step_id"] == "start_addon" + + +async def test_not_addon(hass, supervisor): + """Test opting out of add-on on Supervisor.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": False} + ) + + assert result["type"] == "form" + assert result["step_id"] == "manual" + + with patch( + "homeassistant.components.zwave_js.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.zwave_js.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "url": "ws://localhost:3000", + }, + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == TITLE + assert result["data"] == { + "url": "ws://localhost:3000", + "usb_path": None, + "network_key": None, + "use_addon": False, + "integration_created_addon": False, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) +async def test_addon_running( + hass, + supervisor, + addon_running, + addon_options, + get_addon_discovery_info, +): + """Test add-on already running on Supervisor.""" + addon_options["device"] = "/test" + addon_options["network_key"] = "abc123" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "on_supervisor" + + with patch( + "homeassistant.components.zwave_js.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.zwave_js.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": True} + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == TITLE + assert result["data"] == { + "url": "ws://host1:3001", + "usb_path": "/test", + "network_key": "abc123", + "use_addon": True, + "integration_created_addon": False, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "discovery_info, discovery_info_side_effect, server_version_side_effect, " + "addon_info_side_effect, abort_reason", + [ + ( + {"config": ADDON_DISCOVERY_INFO}, + HassioAPIError(), + None, + None, + "addon_get_discovery_info_failed", + ), + ( + {"config": ADDON_DISCOVERY_INFO}, + None, + asyncio.TimeoutError, + None, + "cannot_connect", + ), + ( + None, + None, + None, + None, + "addon_missing_discovery_info", + ), + ( + {"config": ADDON_DISCOVERY_INFO}, + None, + None, + HassioAPIError(), + "addon_info_failed", + ), + ], +) +async def test_addon_running_failures( + hass, + supervisor, + addon_running, + get_addon_discovery_info, + abort_reason, +): + """Test all failures when add-on is running.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": True} + ) + + assert result["type"] == "abort" + assert result["reason"] == abort_reason + + +@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) +async def test_addon_running_already_configured( + hass, supervisor, addon_running, get_addon_discovery_info +): + """Test that only one unique instance is allowed when add-on is running.""" + entry = MockConfigEntry(domain=DOMAIN, data={}, title=TITLE, unique_id=1234) + entry.add_to_hass(hass) + + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": True} + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) +async def test_addon_installed( + hass, + supervisor, + addon_installed, + addon_options, + set_addon_options, + start_addon, + get_addon_discovery_info, +): + """Test add-on already installed but not running on Supervisor.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": True} + ) + + assert result["type"] == "form" + assert result["step_id"] == "start_addon" + + with patch( + "homeassistant.components.zwave_js.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.zwave_js.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"usb_path": "/test", "network_key": "abc123"} + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == TITLE + assert result["data"] == { + "url": "ws://host1:3001", + "usb_path": "/test", + "network_key": "abc123", + "use_addon": True, + "integration_created_addon": False, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "discovery_info, start_addon_side_effect", + [({"config": ADDON_DISCOVERY_INFO}, HassioAPIError())], +) +async def test_addon_installed_start_failure( + hass, + supervisor, + addon_installed, + addon_options, + set_addon_options, + start_addon, + get_addon_discovery_info, +): + """Test add-on start failure when add-on is installed.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": True} + ) + + assert result["type"] == "form" + assert result["step_id"] == "start_addon" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"usb_path": "/test", "network_key": "abc123"} + ) + + assert result["type"] == "form" + assert result["errors"] == {"base": "addon_start_failed"} + + +@pytest.mark.parametrize( + "set_addon_options_side_effect, start_addon_side_effect, discovery_info, " + "server_version_side_effect, abort_reason", + [ + ( + HassioAPIError(), + None, + {"config": ADDON_DISCOVERY_INFO}, + None, + "addon_set_config_failed", + ), + ( + None, + None, + {"config": ADDON_DISCOVERY_INFO}, + asyncio.TimeoutError, + "cannot_connect", + ), + ( + None, + None, + None, + None, + "addon_missing_discovery_info", + ), + ], +) +async def test_addon_installed_failures( + hass, + supervisor, + addon_installed, + addon_options, + set_addon_options, + start_addon, + get_addon_discovery_info, + abort_reason, +): + """Test all failures when add-on is installed.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": True} + ) + + assert result["type"] == "form" + assert result["step_id"] == "start_addon" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"usb_path": "/test", "network_key": "abc123"} + ) + + assert result["type"] == "abort" + assert result["reason"] == abort_reason + + +@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) +async def test_addon_installed_already_configured( + hass, + supervisor, + addon_installed, + addon_options, + set_addon_options, + start_addon, + get_addon_discovery_info, +): + """Test that only one unique instance is allowed when add-on is installed.""" + entry = MockConfigEntry(domain=DOMAIN, data={}, title=TITLE, unique_id=1234) + entry.add_to_hass(hass) + + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": True} + ) + + assert result["type"] == "form" + assert result["step_id"] == "start_addon" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"usb_path": "/test", "network_key": "abc123"} + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize("discovery_info", [{"config": ADDON_DISCOVERY_INFO}]) +async def test_addon_not_installed( + hass, + supervisor, + addon_installed, + install_addon, + addon_options, + set_addon_options, + start_addon, + get_addon_discovery_info, +): + """Test add-on not installed.""" + addon_installed.return_value["version"] = None + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": True} + ) + + assert result["type"] == "progress" + + # Make sure the flow continues when the progress task is done. + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == "form" + assert result["step_id"] == "start_addon" + + with patch( + "homeassistant.components.zwave_js.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.zwave_js.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"usb_path": "/test", "network_key": "abc123"} + ) + await hass.async_block_till_done() + + assert result["type"] == "create_entry" + assert result["title"] == TITLE + assert result["data"] == { + "url": "ws://host1:3001", + "usb_path": "/test", + "network_key": "abc123", + "use_addon": True, + "integration_created_addon": True, + } + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_install_addon_failure(hass, supervisor, addon_installed, install_addon): + """Test add-on install failure.""" + addon_installed.return_value["version"] = None + install_addon.side_effect = HassioAPIError() + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "on_supervisor" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {"use_addon": True} + ) + + assert result["type"] == "progress" + + # Make sure the flow continues when the progress task is done. + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert result["type"] == "abort" + assert result["reason"] == "addon_install_failed" diff --git a/tests/components/zwave_js/test_cover.py b/tests/components/zwave_js/test_cover.py new file mode 100644 index 00000000000..f014245a5f8 --- /dev/null +++ b/tests/components/zwave_js/test_cover.py @@ -0,0 +1,280 @@ +"""Test the Z-Wave JS cover platform.""" +from zwave_js_server.event import Event + +from homeassistant.components.cover import ATTR_CURRENT_POSITION + +WINDOW_COVER_ENTITY = "cover.zws_12_current_value" + + +async def test_cover(hass, client, chain_actuator_zws12, integration): + """Test the light entity.""" + node = chain_actuator_zws12 + state = hass.states.get(WINDOW_COVER_ENTITY) + + assert state + assert state.state == "closed" + assert state.attributes[ATTR_CURRENT_POSITION] == 0 + + # Test setting position + await hass.services.async_call( + "cover", + "set_cover_position", + {"entity_id": WINDOW_COVER_ENTITY, "position": 50}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 6 + assert args["valueId"] == { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "label": "Target value", + "max": 99, + "min": 0, + "type": "number", + "readable": True, + "writeable": True, + "label": "Target value", + }, + } + assert args["value"] == 50 + + client.async_send_command.reset_mock() + + # Test setting position + await hass.services.async_call( + "cover", + "set_cover_position", + {"entity_id": WINDOW_COVER_ENTITY, "position": 0}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 6 + assert args["valueId"] == { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "label": "Target value", + "max": 99, + "min": 0, + "type": "number", + "readable": True, + "writeable": True, + "label": "Target value", + }, + } + assert args["value"] == 0 + + client.async_send_command.reset_mock() + + # Test opening + await hass.services.async_call( + "cover", + "open_cover", + {"entity_id": WINDOW_COVER_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 6 + assert args["valueId"] == { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "Open", + "propertyName": "Open", + "metadata": { + "type": "boolean", + "readable": True, + "writeable": True, + "label": "Perform a level change (Open)", + "ccSpecific": {"switchType": 3}, + }, + } + assert args["value"] + + client.async_send_command.reset_mock() + # Test stop after opening + await hass.services.async_call( + "cover", + "stop_cover", + {"entity_id": WINDOW_COVER_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 2 + open_args = client.async_send_command.call_args_list[0][0][0] + assert open_args["command"] == "node.set_value" + assert open_args["nodeId"] == 6 + assert open_args["valueId"] == { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "Open", + "propertyName": "Open", + "metadata": { + "type": "boolean", + "readable": True, + "writeable": True, + "label": "Perform a level change (Open)", + "ccSpecific": {"switchType": 3}, + }, + } + assert not open_args["value"] + + close_args = client.async_send_command.call_args_list[1][0][0] + assert close_args["command"] == "node.set_value" + assert close_args["nodeId"] == 6 + assert close_args["valueId"] == { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "Close", + "propertyName": "Close", + "metadata": { + "type": "boolean", + "readable": True, + "writeable": True, + "label": "Perform a level change (Close)", + "ccSpecific": {"switchType": 3}, + }, + } + assert not close_args["value"] + + # Test position update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 6, + "args": { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "currentValue", + "newValue": 99, + "prevValue": 0, + "propertyName": "currentValue", + }, + }, + ) + node.receive_event(event) + client.async_send_command.reset_mock() + + state = hass.states.get(WINDOW_COVER_ENTITY) + assert state.state == "open" + + # Test closing + await hass.services.async_call( + "cover", + "close_cover", + {"entity_id": WINDOW_COVER_ENTITY}, + blocking=True, + ) + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 6 + assert args["valueId"] == { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "Close", + "propertyName": "Close", + "metadata": { + "type": "boolean", + "readable": True, + "writeable": True, + "label": "Perform a level change (Close)", + "ccSpecific": {"switchType": 3}, + }, + } + assert args["value"] + + client.async_send_command.reset_mock() + + # Test stop after closing + await hass.services.async_call( + "cover", + "stop_cover", + {"entity_id": WINDOW_COVER_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 2 + open_args = client.async_send_command.call_args_list[0][0][0] + assert open_args["command"] == "node.set_value" + assert open_args["nodeId"] == 6 + assert open_args["valueId"] == { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "Open", + "propertyName": "Open", + "metadata": { + "type": "boolean", + "readable": True, + "writeable": True, + "label": "Perform a level change (Open)", + "ccSpecific": {"switchType": 3}, + }, + } + assert not open_args["value"] + + close_args = client.async_send_command.call_args_list[1][0][0] + assert close_args["command"] == "node.set_value" + assert close_args["nodeId"] == 6 + assert close_args["valueId"] == { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "Close", + "propertyName": "Close", + "metadata": { + "type": "boolean", + "readable": True, + "writeable": True, + "label": "Perform a level change (Close)", + "ccSpecific": {"switchType": 3}, + }, + } + assert not close_args["value"] + + client.async_send_command.reset_mock() + + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 6, + "args": { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "currentValue", + "newValue": 0, + "prevValue": 0, + "propertyName": "currentValue", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(WINDOW_COVER_ENTITY) + assert state.state == "closed" diff --git a/tests/components/zwave_js/test_events.py b/tests/components/zwave_js/test_events.py new file mode 100644 index 00000000000..2a347f6afea --- /dev/null +++ b/tests/components/zwave_js/test_events.py @@ -0,0 +1,158 @@ +"""Test Z-Wave JS (value notification) events.""" +from zwave_js_server.event import Event + +from tests.common import async_capture_events + + +async def test_scenes(hass, hank_binary_switch, integration, client): + """Test scene events.""" + # just pick a random node to fake the value notification events + node = hank_binary_switch + events = async_capture_events(hass, "zwave_js_event") + + # Publish fake Basic Set value notification + event = Event( + type="value notification", + data={ + "source": "node", + "event": "value notification", + "nodeId": 32, + "args": { + "commandClassName": "Basic", + "commandClass": 32, + "endpoint": 0, + "property": "event", + "propertyName": "event", + "value": 255, + "metadata": { + "type": "number", + "readable": True, + "writeable": False, + "min": 0, + "max": 255, + "label": "Event value", + }, + "ccVersion": 1, + }, + }, + ) + node.receive_event(event) + # wait for the event + await hass.async_block_till_done() + assert len(events) == 1 + assert events[0].data["home_id"] == client.driver.controller.home_id + assert events[0].data["node_id"] == 32 + assert events[0].data["endpoint"] == 0 + assert events[0].data["command_class"] == 32 + assert events[0].data["command_class_name"] == "Basic" + assert events[0].data["label"] == "Event value" + assert events[0].data["value"] == 255 + + # Publish fake Scene Activation value notification + event = Event( + type="value notification", + data={ + "source": "node", + "event": "value notification", + "nodeId": 32, + "args": { + "commandClassName": "Scene Activation", + "commandClass": 43, + "endpoint": 0, + "property": "SceneID", + "propertyName": "SceneID", + "value": 16, + "metadata": { + "type": "number", + "readable": True, + "writeable": False, + "min": 0, + "max": 255, + "label": "Scene ID", + }, + "ccVersion": 3, + }, + }, + ) + node.receive_event(event) + # wait for the event + await hass.async_block_till_done() + assert len(events) == 2 + assert events[1].data["command_class"] == 43 + assert events[1].data["command_class_name"] == "Scene Activation" + assert events[1].data["label"] == "Scene ID" + assert events[1].data["value"] == 16 + + # Publish fake Central Scene value notification + event = Event( + type="value notification", + data={ + "source": "node", + "event": "value notification", + "nodeId": 32, + "args": { + "commandClassName": "Central Scene", + "commandClass": 91, + "endpoint": 0, + "property": "scene", + "propertyKey": "001", + "propertyName": "scene", + "propertyKeyName": "001", + "value": 4, + "metadata": { + "type": "number", + "readable": True, + "writeable": False, + "min": 0, + "max": 255, + "label": "Scene 001", + "states": { + "0": "KeyPressed", + "1": "KeyReleased", + "2": "KeyHeldDown", + "3": "KeyPressed2x", + "4": "KeyPressed3x", + "5": "KeyPressed4x", + "6": "KeyPressed5x", + }, + }, + "ccVersion": 3, + }, + }, + ) + node.receive_event(event) + # wait for the event + await hass.async_block_till_done() + assert len(events) == 3 + assert events[2].data["command_class"] == 91 + assert events[2].data["command_class_name"] == "Central Scene" + assert events[2].data["label"] == "Scene 001" + assert events[2].data["value"] == "KeyPressed3x" + + +async def test_notifications(hass, hank_binary_switch, integration, client): + """Test notification events.""" + # just pick a random node to fake the value notification events + node = hank_binary_switch + events = async_capture_events(hass, "zwave_js_event") + + # Publish fake Basic Set value notification + event = Event( + type="notification", + data={ + "source": "node", + "event": "notification", + "nodeId": 23, + "notificationLabel": "Keypad lock operation", + "parameters": {"userId": 1}, + }, + ) + node.receive_event(event) + # wait for the event + await hass.async_block_till_done() + assert len(events) == 1 + assert events[0].data["type"] == "notification" + assert events[0].data["home_id"] == client.driver.controller.home_id + assert events[0].data["node_id"] == 32 + assert events[0].data["label"] == "Keypad lock operation" + assert events[0].data["parameters"]["userId"] == 1 diff --git a/tests/components/zwave_js/test_fan.py b/tests/components/zwave_js/test_fan.py new file mode 100644 index 00000000000..5b726179ac9 --- /dev/null +++ b/tests/components/zwave_js/test_fan.py @@ -0,0 +1,172 @@ +"""Test the Z-Wave JS fan platform.""" +import pytest +from zwave_js_server.event import Event + +from homeassistant.components.fan import ATTR_SPEED, SPEED_MEDIUM + +FAN_ENTITY = "fan.in_wall_smart_fan_control_current_value" + + +async def test_fan(hass, client, in_wall_smart_fan_control, integration): + """Test the fan entity.""" + node = in_wall_smart_fan_control + state = hass.states.get(FAN_ENTITY) + + assert state + assert state.state == "off" + + # Test turn on setting speed + await hass.services.async_call( + "fan", + "turn_on", + {"entity_id": FAN_ENTITY, "speed": SPEED_MEDIUM}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 17 + assert args["valueId"] == { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "label": "Target value", + "max": 99, + "min": 0, + "type": "number", + "readable": True, + "writeable": True, + "label": "Target value", + }, + } + assert args["value"] == 50 + + client.async_send_command.reset_mock() + + # Test setting unknown speed + with pytest.raises(ValueError): + await hass.services.async_call( + "fan", + "set_speed", + {"entity_id": FAN_ENTITY, "speed": 99}, + blocking=True, + ) + + client.async_send_command.reset_mock() + + # Test turn on no speed + await hass.services.async_call( + "fan", + "turn_on", + {"entity_id": FAN_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 17 + assert args["valueId"] == { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "label": "Target value", + "max": 99, + "min": 0, + "type": "number", + "readable": True, + "writeable": True, + "label": "Target value", + }, + } + assert args["value"] == 255 + + client.async_send_command.reset_mock() + + # Test turning off + await hass.services.async_call( + "fan", + "turn_off", + {"entity_id": FAN_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 17 + assert args["valueId"] == { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "label": "Target value", + "max": 99, + "min": 0, + "type": "number", + "readable": True, + "writeable": True, + "label": "Target value", + }, + } + assert args["value"] == 0 + + client.async_send_command.reset_mock() + + # Test speed update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 17, + "args": { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "currentValue", + "newValue": 99, + "prevValue": 0, + "propertyName": "currentValue", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(FAN_ENTITY) + assert state.state == "on" + assert state.attributes[ATTR_SPEED] == "high" + + client.async_send_command.reset_mock() + + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 17, + "args": { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "currentValue", + "newValue": 0, + "prevValue": 0, + "propertyName": "currentValue", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(FAN_ENTITY) + assert state.state == "off" + assert state.attributes[ATTR_SPEED] == "off" diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py new file mode 100644 index 00000000000..1aad07400ad --- /dev/null +++ b/tests/components/zwave_js/test_init.py @@ -0,0 +1,295 @@ +"""Test the Z-Wave JS init module.""" +from copy import deepcopy +from unittest.mock import patch + +import pytest +from zwave_js_server.model.node import Node + +from homeassistant.components.hassio.handler import HassioAPIError +from homeassistant.components.zwave_js.const import DOMAIN +from homeassistant.components.zwave_js.entity import get_device_id +from homeassistant.config_entries import ( + CONN_CLASS_LOCAL_PUSH, + ENTRY_STATE_LOADED, + ENTRY_STATE_NOT_LOADED, + ENTRY_STATE_SETUP_RETRY, +) +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.helpers import device_registry, entity_registry + +from .common import AIR_TEMPERATURE_SENSOR + +from tests.common import MockConfigEntry + + +@pytest.fixture(name="connect_timeout") +def connect_timeout_fixture(): + """Mock the connect timeout.""" + with patch("homeassistant.components.zwave_js.CONNECT_TIMEOUT", new=0) as timeout: + yield timeout + + +@pytest.fixture(name="stop_addon") +def stop_addon_fixture(): + """Mock stop add-on.""" + with patch("homeassistant.components.hassio.async_stop_addon") as stop_addon: + yield stop_addon + + +@pytest.fixture(name="uninstall_addon") +def uninstall_addon_fixture(): + """Mock uninstall add-on.""" + with patch( + "homeassistant.components.hassio.async_uninstall_addon" + ) as uninstall_addon: + yield uninstall_addon + + +async def test_entry_setup_unload(hass, client, integration): + """Test the integration set up and unload.""" + entry = integration + + assert client.connect.call_count == 1 + assert entry.state == ENTRY_STATE_LOADED + + await hass.config_entries.async_unload(entry.entry_id) + + assert client.disconnect.call_count == 1 + assert entry.state == ENTRY_STATE_NOT_LOADED + + +async def test_home_assistant_stop(hass, client, integration): + """Test we clean up on home assistant stop.""" + await hass.async_stop() + + assert client.disconnect.call_count == 1 + + +async def test_initialized_timeout(hass, client, connect_timeout): + """Test we handle a timeout during client initialization.""" + entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"}) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_on_node_added_ready( + hass, multisensor_6_state, client, integration, device_registry +): + """Test we handle a ready node added event.""" + node = Node(client, multisensor_6_state) + event = {"node": node} + air_temperature_device_id = f"{client.driver.controller.home_id}-{node.node_id}" + + state = hass.states.get(AIR_TEMPERATURE_SENSOR) + + assert not state # entity and device not yet added + assert not device_registry.async_get_device( + identifiers={(DOMAIN, air_temperature_device_id)} + ) + + client.driver.controller.emit("node added", event) + await hass.async_block_till_done() + + state = hass.states.get(AIR_TEMPERATURE_SENSOR) + + assert state # entity and device added + assert state.state != STATE_UNAVAILABLE + assert device_registry.async_get_device( + identifiers={(DOMAIN, air_temperature_device_id)} + ) + + +async def test_on_node_added_not_ready( + hass, multisensor_6_state, client, integration, device_registry +): + """Test we handle a non ready node added event.""" + node_data = deepcopy(multisensor_6_state) # Copy to allow modification in tests. + node = Node(client, node_data) + node.data["ready"] = False + event = {"node": node} + air_temperature_device_id = f"{client.driver.controller.home_id}-{node.node_id}" + + state = hass.states.get(AIR_TEMPERATURE_SENSOR) + + assert not state # entity and device not yet added + assert not device_registry.async_get_device( + identifiers={(DOMAIN, air_temperature_device_id)} + ) + + client.driver.controller.emit("node added", event) + await hass.async_block_till_done() + + state = hass.states.get(AIR_TEMPERATURE_SENSOR) + + assert not state # entity not yet added but device added in registry + assert device_registry.async_get_device( + identifiers={(DOMAIN, air_temperature_device_id)} + ) + + node.data["ready"] = True + node.emit("ready", event) + await hass.async_block_till_done() + + state = hass.states.get(AIR_TEMPERATURE_SENSOR) + + assert state # entity added + assert state.state != STATE_UNAVAILABLE + + +async def test_existing_node_ready( + hass, client, multisensor_6, integration, device_registry +): + """Test we handle a ready node that exists during integration setup.""" + node = multisensor_6 + air_temperature_device_id = f"{client.driver.controller.home_id}-{node.node_id}" + + state = hass.states.get(AIR_TEMPERATURE_SENSOR) + + assert state # entity and device added + assert state.state != STATE_UNAVAILABLE + assert device_registry.async_get_device( + identifiers={(DOMAIN, air_temperature_device_id)} + ) + + +async def test_existing_node_not_ready(hass, client, multisensor_6, device_registry): + """Test we handle a non ready node that exists during integration setup.""" + node = multisensor_6 + node.data = deepcopy(node.data) # Copy to allow modification in tests. + node.data["ready"] = False + event = {"node": node} + air_temperature_device_id = f"{client.driver.controller.home_id}-{node.node_id}" + entry = MockConfigEntry(domain="zwave_js", data={"url": "ws://test.org"}) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(AIR_TEMPERATURE_SENSOR) + + assert not state # entity not yet added + assert device_registry.async_get_device( # device should be added + identifiers={(DOMAIN, air_temperature_device_id)} + ) + + node.data["ready"] = True + node.emit("ready", event) + await hass.async_block_till_done() + + state = hass.states.get(AIR_TEMPERATURE_SENSOR) + + assert state # entity and device added + assert state.state != STATE_UNAVAILABLE + assert device_registry.async_get_device( + identifiers={(DOMAIN, air_temperature_device_id)} + ) + + +async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog): + """Test remove the config entry.""" + # test successful remove without created add-on + entry = MockConfigEntry( + domain=DOMAIN, + title="Z-Wave JS", + connection_class=CONN_CLASS_LOCAL_PUSH, + data={"integration_created_addon": False}, + ) + entry.add_to_hass(hass) + assert entry.state == ENTRY_STATE_NOT_LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + await hass.config_entries.async_remove(entry.entry_id) + + assert entry.state == ENTRY_STATE_NOT_LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 0 + + # test successful remove with created add-on + entry = MockConfigEntry( + domain=DOMAIN, + title="Z-Wave JS", + connection_class=CONN_CLASS_LOCAL_PUSH, + data={"integration_created_addon": True}, + ) + entry.add_to_hass(hass) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + await hass.config_entries.async_remove(entry.entry_id) + + assert stop_addon.call_count == 1 + assert uninstall_addon.call_count == 1 + assert entry.state == ENTRY_STATE_NOT_LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 0 + stop_addon.reset_mock() + uninstall_addon.reset_mock() + + # test add-on stop failure + entry.add_to_hass(hass) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + stop_addon.side_effect = HassioAPIError() + + await hass.config_entries.async_remove(entry.entry_id) + + assert stop_addon.call_count == 1 + assert uninstall_addon.call_count == 0 + assert entry.state == ENTRY_STATE_NOT_LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 0 + assert "Failed to stop the Z-Wave JS add-on" in caplog.text + stop_addon.side_effect = None + stop_addon.reset_mock() + uninstall_addon.reset_mock() + + # test add-on uninstall failure + entry.add_to_hass(hass) + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + uninstall_addon.side_effect = HassioAPIError() + + await hass.config_entries.async_remove(entry.entry_id) + + assert stop_addon.call_count == 1 + assert uninstall_addon.call_count == 1 + assert entry.state == ENTRY_STATE_NOT_LOADED + assert len(hass.config_entries.async_entries(DOMAIN)) == 0 + assert "Failed to uninstall the Z-Wave JS add-on" in caplog.text + + +async def test_removed_device(hass, client, multiple_devices, integration): + """Test that the device registry gets updated when a device gets removed.""" + nodes = multiple_devices + + # Verify how many nodes are available + assert len(client.driver.controller.nodes) == 2 + + # Make sure there are the same number of devices + dev_reg = await device_registry.async_get_registry(hass) + device_entries = device_registry.async_entries_for_config_entry( + dev_reg, integration.entry_id + ) + assert len(device_entries) == 2 + + # Check how many entities there are + ent_reg = await entity_registry.async_get_registry(hass) + entity_entries = entity_registry.async_entries_for_config_entry( + ent_reg, integration.entry_id + ) + assert len(entity_entries) == 24 + + # Remove a node and reload the entry + old_node = nodes.pop(13) + await hass.config_entries.async_reload(integration.entry_id) + await hass.async_block_till_done() + + # Assert that the node and all of it's entities were removed from the device and + # entity registry + device_entries = device_registry.async_entries_for_config_entry( + dev_reg, integration.entry_id + ) + assert len(device_entries) == 1 + entity_entries = entity_registry.async_entries_for_config_entry( + ent_reg, integration.entry_id + ) + assert len(entity_entries) == 15 + assert dev_reg.async_get_device({get_device_id(client, old_node)}) is None diff --git a/tests/components/zwave_js/test_light.py b/tests/components/zwave_js/test_light.py new file mode 100644 index 00000000000..b60c7281874 --- /dev/null +++ b/tests/components/zwave_js/test_light.py @@ -0,0 +1,399 @@ +"""Test the Z-Wave JS light platform.""" +from copy import deepcopy + +from zwave_js_server.event import Event + +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_MAX_MIREDS, + ATTR_MIN_MIREDS, + ATTR_RGB_COLOR, +) +from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON + +BULB_6_MULTI_COLOR_LIGHT_ENTITY = "light.bulb_6_multi_color_current_value" +EATON_RF9640_ENTITY = "light.allloaddimmer_current_value" + + +async def test_light(hass, client, bulb_6_multi_color, integration): + """Test the light entity.""" + node = bulb_6_multi_color + state = hass.states.get(BULB_6_MULTI_COLOR_LIGHT_ENTITY) + + assert state + assert state.state == STATE_OFF + assert state.attributes[ATTR_MIN_MIREDS] == 153 + assert state.attributes[ATTR_MAX_MIREDS] == 370 + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 51 + + # Test turning on + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": BULB_6_MULTI_COLOR_LIGHT_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 39 + assert args["valueId"] == { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "label": "Target value", + "max": 99, + "min": 0, + "type": "number", + "readable": True, + "writeable": True, + "label": "Target value", + }, + } + assert args["value"] == 255 + + client.async_send_command.reset_mock() + + # Test brightness update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 39, + "args": { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "currentValue", + "newValue": 99, + "prevValue": 0, + "propertyName": "currentValue", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(BULB_6_MULTI_COLOR_LIGHT_ENTITY) + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 255 + assert state.attributes[ATTR_COLOR_TEMP] == 370 + + # Test turning on with same brightness + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": BULB_6_MULTI_COLOR_LIGHT_ENTITY, ATTR_BRIGHTNESS: 255}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + + client.async_send_command.reset_mock() + + # Test turning on with brightness + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": BULB_6_MULTI_COLOR_LIGHT_ENTITY, ATTR_BRIGHTNESS: 129}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 39 + assert args["valueId"] == { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "label": "Target value", + "max": 99, + "min": 0, + "type": "number", + "readable": True, + "writeable": True, + "label": "Target value", + }, + } + assert args["value"] == 50 + + client.async_send_command.reset_mock() + + # Test turning on with rgb color + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": BULB_6_MULTI_COLOR_LIGHT_ENTITY, ATTR_RGB_COLOR: (255, 76, 255)}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 5 + warm_args = client.async_send_command.call_args_list[0][0][0] # warm white 0 + assert warm_args["command"] == "node.set_value" + assert warm_args["nodeId"] == 39 + assert warm_args["valueId"]["commandClassName"] == "Color Switch" + assert warm_args["valueId"]["commandClass"] == 51 + assert warm_args["valueId"]["endpoint"] == 0 + assert warm_args["valueId"]["metadata"]["label"] == "Target value (Warm White)" + assert warm_args["valueId"]["property"] == "targetColor" + assert warm_args["valueId"]["propertyName"] == "targetColor" + assert warm_args["value"] == 0 + + cold_args = client.async_send_command.call_args_list[1][0][0] # cold white 0 + assert cold_args["command"] == "node.set_value" + assert cold_args["nodeId"] == 39 + assert cold_args["valueId"]["commandClassName"] == "Color Switch" + assert cold_args["valueId"]["commandClass"] == 51 + assert cold_args["valueId"]["endpoint"] == 0 + assert cold_args["valueId"]["metadata"]["label"] == "Target value (Cold White)" + assert cold_args["valueId"]["property"] == "targetColor" + assert cold_args["valueId"]["propertyName"] == "targetColor" + assert cold_args["value"] == 0 + red_args = client.async_send_command.call_args_list[2][0][0] # red 255 + assert red_args["command"] == "node.set_value" + assert red_args["nodeId"] == 39 + assert red_args["valueId"]["commandClassName"] == "Color Switch" + assert red_args["valueId"]["commandClass"] == 51 + assert red_args["valueId"]["endpoint"] == 0 + assert red_args["valueId"]["metadata"]["label"] == "Target value (Red)" + assert red_args["valueId"]["property"] == "targetColor" + assert red_args["valueId"]["propertyName"] == "targetColor" + assert red_args["value"] == 255 + green_args = client.async_send_command.call_args_list[3][0][0] # green 76 + assert green_args["command"] == "node.set_value" + assert green_args["nodeId"] == 39 + assert green_args["valueId"]["commandClassName"] == "Color Switch" + assert green_args["valueId"]["commandClass"] == 51 + assert green_args["valueId"]["endpoint"] == 0 + assert green_args["valueId"]["metadata"]["label"] == "Target value (Green)" + assert green_args["valueId"]["property"] == "targetColor" + assert green_args["valueId"]["propertyName"] == "targetColor" + assert green_args["value"] == 76 + blue_args = client.async_send_command.call_args_list[4][0][0] # blue 255 + assert blue_args["command"] == "node.set_value" + assert blue_args["nodeId"] == 39 + assert blue_args["valueId"]["commandClassName"] == "Color Switch" + assert blue_args["valueId"]["commandClass"] == 51 + assert blue_args["valueId"]["endpoint"] == 0 + assert blue_args["valueId"]["metadata"]["label"] == "Target value (Blue)" + assert blue_args["valueId"]["property"] == "targetColor" + assert blue_args["valueId"]["propertyName"] == "targetColor" + assert blue_args["value"] == 255 + + # Test rgb color update from value updated event + red_event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 39, + "args": { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "currentColor", + "newValue": 255, + "prevValue": 0, + "propertyKeyName": "Red", + }, + }, + ) + green_event = deepcopy(red_event) + green_event.data["args"].update({"newValue": 76, "propertyKeyName": "Green"}) + blue_event = deepcopy(red_event) + blue_event.data["args"]["propertyKeyName"] = "Blue" + warm_white_event = deepcopy(red_event) + warm_white_event.data["args"].update( + {"newValue": 0, "propertyKeyName": "Warm White"} + ) + node.receive_event(warm_white_event) + node.receive_event(red_event) + node.receive_event(green_event) + node.receive_event(blue_event) + + state = hass.states.get(BULB_6_MULTI_COLOR_LIGHT_ENTITY) + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 255 + assert state.attributes[ATTR_COLOR_TEMP] == 370 + assert state.attributes[ATTR_RGB_COLOR] == (255, 76, 255) + + client.async_send_command.reset_mock() + + # Test turning on with same rgb color + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": BULB_6_MULTI_COLOR_LIGHT_ENTITY, ATTR_RGB_COLOR: (255, 76, 255)}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 5 + + client.async_send_command.reset_mock() + + # Test turning on with color temp + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": BULB_6_MULTI_COLOR_LIGHT_ENTITY, ATTR_COLOR_TEMP: 170}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 5 + red_args = client.async_send_command.call_args_list[0][0][0] # red 0 + assert red_args["command"] == "node.set_value" + assert red_args["nodeId"] == 39 + assert red_args["valueId"]["commandClassName"] == "Color Switch" + assert red_args["valueId"]["commandClass"] == 51 + assert red_args["valueId"]["endpoint"] == 0 + assert red_args["valueId"]["metadata"]["label"] == "Target value (Red)" + assert red_args["valueId"]["property"] == "targetColor" + assert red_args["valueId"]["propertyName"] == "targetColor" + assert red_args["value"] == 0 + red_args = client.async_send_command.call_args_list[1][0][0] # green 0 + assert red_args["command"] == "node.set_value" + assert red_args["nodeId"] == 39 + assert red_args["valueId"]["commandClassName"] == "Color Switch" + assert red_args["valueId"]["commandClass"] == 51 + assert red_args["valueId"]["endpoint"] == 0 + assert red_args["valueId"]["metadata"]["label"] == "Target value (Green)" + assert red_args["valueId"]["property"] == "targetColor" + assert red_args["valueId"]["propertyName"] == "targetColor" + assert red_args["value"] == 0 + red_args = client.async_send_command.call_args_list[2][0][0] # blue 0 + assert red_args["command"] == "node.set_value" + assert red_args["nodeId"] == 39 + assert red_args["valueId"]["commandClassName"] == "Color Switch" + assert red_args["valueId"]["commandClass"] == 51 + assert red_args["valueId"]["endpoint"] == 0 + assert red_args["valueId"]["metadata"]["label"] == "Target value (Blue)" + assert red_args["valueId"]["property"] == "targetColor" + assert red_args["valueId"]["propertyName"] == "targetColor" + assert red_args["value"] == 0 + warm_args = client.async_send_command.call_args_list[3][0][0] # warm white 0 + assert warm_args["command"] == "node.set_value" + assert warm_args["nodeId"] == 39 + assert warm_args["valueId"]["commandClassName"] == "Color Switch" + assert warm_args["valueId"]["commandClass"] == 51 + assert warm_args["valueId"]["endpoint"] == 0 + assert warm_args["valueId"]["metadata"]["label"] == "Target value (Warm White)" + assert warm_args["valueId"]["property"] == "targetColor" + assert warm_args["valueId"]["propertyName"] == "targetColor" + assert warm_args["value"] == 20 + red_args = client.async_send_command.call_args_list[4][0][0] # cold white + assert red_args["command"] == "node.set_value" + assert red_args["nodeId"] == 39 + assert red_args["valueId"]["commandClassName"] == "Color Switch" + assert red_args["valueId"]["commandClass"] == 51 + assert red_args["valueId"]["endpoint"] == 0 + assert red_args["valueId"]["metadata"]["label"] == "Target value (Cold White)" + assert red_args["valueId"]["property"] == "targetColor" + assert red_args["valueId"]["propertyName"] == "targetColor" + assert red_args["value"] == 235 + + client.async_send_command.reset_mock() + + # Test color temp update from value updated event + red_event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 39, + "args": { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "currentColor", + "newValue": 0, + "prevValue": 255, + "propertyKeyName": "Red", + }, + }, + ) + green_event = deepcopy(red_event) + green_event.data["args"].update( + {"newValue": 0, "prevValue": 76, "propertyKeyName": "Green"} + ) + blue_event = deepcopy(red_event) + blue_event.data["args"]["propertyKeyName"] = "Blue" + warm_white_event = deepcopy(red_event) + warm_white_event.data["args"].update( + {"newValue": 20, "propertyKeyName": "Warm White"} + ) + cold_white_event = deepcopy(red_event) + cold_white_event.data["args"].update( + {"newValue": 235, "propertyKeyName": "Cold White"} + ) + node.receive_event(red_event) + node.receive_event(green_event) + node.receive_event(blue_event) + node.receive_event(warm_white_event) + node.receive_event(cold_white_event) + + state = hass.states.get(BULB_6_MULTI_COLOR_LIGHT_ENTITY) + assert state.state == STATE_ON + assert state.attributes[ATTR_BRIGHTNESS] == 255 + assert state.attributes[ATTR_COLOR_TEMP] == 170 + assert state.attributes[ATTR_RGB_COLOR] == (255, 255, 255) + + # Test turning on with same color temp + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": BULB_6_MULTI_COLOR_LIGHT_ENTITY, ATTR_COLOR_TEMP: 170}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 5 + + client.async_send_command.reset_mock() + + # Test turning off + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": BULB_6_MULTI_COLOR_LIGHT_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 39 + assert args["valueId"] == { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "label": "Target value", + "max": 99, + "min": 0, + "type": "number", + "readable": True, + "writeable": True, + "label": "Target value", + }, + } + assert args["value"] == 0 + + +async def test_v4_dimmer_light(hass, client, eaton_rf9640_dimmer, integration): + """Test a light that supports MultiLevelSwitch CommandClass version 4.""" + state = hass.states.get(EATON_RF9640_ENTITY) + + assert state + assert state.state == STATE_ON + # the light should pick currentvalue which has zwave value 22 + assert state.attributes[ATTR_BRIGHTNESS] == 57 diff --git a/tests/components/zwave_js/test_lock.py b/tests/components/zwave_js/test_lock.py new file mode 100644 index 00000000000..069b3497a55 --- /dev/null +++ b/tests/components/zwave_js/test_lock.py @@ -0,0 +1,205 @@ +"""Test the Z-Wave JS lock platform.""" +from zwave_js_server.const import ATTR_CODE_SLOT, ATTR_USERCODE +from zwave_js_server.event import Event + +from homeassistant.components.lock import ( + DOMAIN as LOCK_DOMAIN, + SERVICE_LOCK, + SERVICE_UNLOCK, +) +from homeassistant.components.zwave_js.const import DOMAIN as ZWAVE_JS_DOMAIN +from homeassistant.components.zwave_js.lock import ( + SERVICE_CLEAR_LOCK_USERCODE, + SERVICE_SET_LOCK_USERCODE, +) +from homeassistant.const import ATTR_ENTITY_ID, STATE_LOCKED, STATE_UNLOCKED + +SCHLAGE_BE469_LOCK_ENTITY = "lock.touchscreen_deadbolt_current_lock_mode" + + +async def test_door_lock(hass, client, lock_schlage_be469, integration): + """Test a lock entity with door lock command class.""" + node = lock_schlage_be469 + state = hass.states.get(SCHLAGE_BE469_LOCK_ENTITY) + + assert state + assert state.state == STATE_UNLOCKED + + # Test locking + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_LOCK, + {ATTR_ENTITY_ID: SCHLAGE_BE469_LOCK_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 20 + assert args["valueId"] == { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "targetMode", + "propertyName": "targetMode", + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "min": 0, + "max": 255, + "label": "Target lock mode", + "states": { + "0": "Unsecured", + "1": "UnsecuredWithTimeout", + "16": "InsideUnsecured", + "17": "InsideUnsecuredWithTimeout", + "32": "OutsideUnsecured", + "33": "OutsideUnsecuredWithTimeout", + "254": "Unknown", + "255": "Secured", + }, + }, + } + assert args["value"] == 255 + + client.async_send_command.reset_mock() + + # Test locked update from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 20, + "args": { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "currentMode", + "newValue": 255, + "prevValue": 0, + "propertyName": "currentMode", + }, + }, + ) + node.receive_event(event) + + assert hass.states.get(SCHLAGE_BE469_LOCK_ENTITY).state == STATE_LOCKED + + client.async_send_command.reset_mock() + + # Test unlocking + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_UNLOCK, + {ATTR_ENTITY_ID: SCHLAGE_BE469_LOCK_ENTITY}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 20 + assert args["valueId"] == { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "targetMode", + "propertyName": "targetMode", + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "min": 0, + "max": 255, + "label": "Target lock mode", + "states": { + "0": "Unsecured", + "1": "UnsecuredWithTimeout", + "16": "InsideUnsecured", + "17": "InsideUnsecuredWithTimeout", + "32": "OutsideUnsecured", + "33": "OutsideUnsecuredWithTimeout", + "254": "Unknown", + "255": "Secured", + }, + }, + } + assert args["value"] == 0 + + client.async_send_command.reset_mock() + + # Test set usercode service + await hass.services.async_call( + ZWAVE_JS_DOMAIN, + SERVICE_SET_LOCK_USERCODE, + { + ATTR_ENTITY_ID: SCHLAGE_BE469_LOCK_ENTITY, + ATTR_CODE_SLOT: 1, + ATTR_USERCODE: "1234", + }, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 20 + assert args["valueId"] == { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyName": "userCode", + "propertyKey": 1, + "propertyKeyName": "1", + "metadata": { + "type": "string", + "readable": True, + "writeable": True, + "minLength": 4, + "maxLength": 10, + "label": "User Code (1)", + }, + "value": "**********", + } + assert args["value"] == "1234" + + client.async_send_command.reset_mock() + + # Test clear usercode + await hass.services.async_call( + ZWAVE_JS_DOMAIN, + SERVICE_CLEAR_LOCK_USERCODE, + {ATTR_ENTITY_ID: SCHLAGE_BE469_LOCK_ENTITY, ATTR_CODE_SLOT: 1}, + blocking=True, + ) + + assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 20 + assert args["valueId"] == { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyName": "userIdStatus", + "propertyKey": 1, + "propertyKeyName": "1", + "metadata": { + "type": "number", + "readable": True, + "writeable": True, + "label": "User ID status (1)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled", + }, + }, + "value": 1, + } + assert args["value"] == 0 diff --git a/tests/components/zwave_js/test_sensor.py b/tests/components/zwave_js/test_sensor.py new file mode 100644 index 00000000000..bd6fb9f2569 --- /dev/null +++ b/tests/components/zwave_js/test_sensor.py @@ -0,0 +1,72 @@ +"""Test the Z-Wave JS sensor platform.""" +from homeassistant.const import ( + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_TEMPERATURE, + ENERGY_KILO_WATT_HOUR, + POWER_WATT, + TEMP_CELSIUS, +) +from homeassistant.helpers.entity_registry import ( + DISABLED_INTEGRATION, + async_get_registry, +) + +from .common import ( + AIR_TEMPERATURE_SENSOR, + ENERGY_SENSOR, + NOTIFICATION_MOTION_SENSOR, + POWER_SENSOR, +) + + +async def test_numeric_sensor(hass, multisensor_6, integration): + """Test the numeric sensor.""" + state = hass.states.get(AIR_TEMPERATURE_SENSOR) + + assert state + assert state.state == "9.0" + assert state.attributes["unit_of_measurement"] == TEMP_CELSIUS + assert state.attributes["device_class"] == DEVICE_CLASS_TEMPERATURE + + +async def test_energy_sensors(hass, hank_binary_switch, integration): + """Test power and energy sensors.""" + state = hass.states.get(POWER_SENSOR) + + assert state + assert state.state == "0.0" + assert state.attributes["unit_of_measurement"] == POWER_WATT + assert state.attributes["device_class"] == DEVICE_CLASS_POWER + + state = hass.states.get(ENERGY_SENSOR) + + assert state + assert state.state == "0.16" + assert state.attributes["unit_of_measurement"] == ENERGY_KILO_WATT_HOUR + assert state.attributes["device_class"] == DEVICE_CLASS_ENERGY + + +async def test_disabled_notification_sensor(hass, multisensor_6, integration): + """Test sensor is created from Notification CC and is disabled.""" + ent_reg = await async_get_registry(hass) + entity_entry = ent_reg.async_get(NOTIFICATION_MOTION_SENSOR) + + assert entity_entry + assert entity_entry.disabled + assert entity_entry.disabled_by == DISABLED_INTEGRATION + + # Test enabling entity + updated_entry = ent_reg.async_update_entity( + entity_entry.entity_id, **{"disabled_by": None} + ) + assert updated_entry != entity_entry + assert updated_entry.disabled is False + + # reload integration and check if entity is correctly there + await hass.config_entries.async_reload(integration.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(NOTIFICATION_MOTION_SENSOR) + assert state.state == "Motion detection" + assert state.attributes["value"] == 8 diff --git a/tests/components/zwave_js/test_switch.py b/tests/components/zwave_js/test_switch.py new file mode 100644 index 00000000000..a1d177cc5d8 --- /dev/null +++ b/tests/components/zwave_js/test_switch.py @@ -0,0 +1,85 @@ +"""Test the Z-Wave JS switch platform.""" + +from zwave_js_server.event import Event + +from .common import SWITCH_ENTITY + + +async def test_switch(hass, hank_binary_switch, integration, client): + """Test the switch.""" + state = hass.states.get(SWITCH_ENTITY) + node = hank_binary_switch + + assert state + assert state.state == "off" + + # Test turning on + await hass.services.async_call( + "switch", "turn_on", {"entity_id": SWITCH_ENTITY}, blocking=True + ) + + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 32 + assert args["valueId"] == { + "commandClassName": "Binary Switch", + "commandClass": 37, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "type": "boolean", + "readable": True, + "writeable": True, + "label": "Target value", + }, + "value": False, + } + assert args["value"] is True + + # Test state updates from value updated event + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 32, + "args": { + "commandClassName": "Binary Switch", + "commandClass": 37, + "endpoint": 0, + "property": "currentValue", + "newValue": True, + "prevValue": False, + "propertyName": "currentValue", + }, + }, + ) + node.receive_event(event) + + state = hass.states.get(SWITCH_ENTITY) + assert state.state == "on" + + # Test turning off + await hass.services.async_call( + "switch", "turn_off", {"entity_id": SWITCH_ENTITY}, blocking=True + ) + + args = client.async_send_command.call_args[0][0] + assert args["command"] == "node.set_value" + assert args["nodeId"] == 32 + assert args["valueId"] == { + "commandClassName": "Binary Switch", + "commandClass": 37, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "type": "boolean", + "readable": True, + "writeable": True, + "label": "Target value", + }, + "value": False, + } + assert args["value"] is False diff --git a/tests/conftest.py b/tests/conftest.py index d8fb9f2914b..55249a58fc9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,6 +5,7 @@ import functools import logging import ssl import threading +from unittest.mock import MagicMock, patch from aiohttp.test_utils import make_mocked_request import multidict @@ -27,7 +28,6 @@ from homeassistant.helpers import config_entry_oauth2_flow, event from homeassistant.setup import async_setup_component from homeassistant.util import location -from tests.async_mock import MagicMock, patch from tests.ignore_uncaught_exceptions import IGNORE_UNCAUGHT_EXCEPTIONS pytest.register_assert_rewrite("tests.common") diff --git a/tests/fixtures/homekit_controller/koogeek_p1eu.json b/tests/fixtures/homekit_controller/koogeek_p1eu.json new file mode 100644 index 00000000000..d9d252b4cb7 --- /dev/null +++ b/tests/fixtures/homekit_controller/koogeek_p1eu.json @@ -0,0 +1,392 @@ +[ + { + "aid": 1, + "services": [ + { + "characteristics": [ + { + "format": "string", + "iid": 2, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "Koogeek-P1-A00AA0" + }, + { + "format": "string", + "iid": 3, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "00000020-0000-1000-8000-0026BB765291", + "value": "Koogeek" + }, + { + "format": "string", + "iid": 4, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "00000021-0000-1000-8000-0026BB765291", + "value": "P1EU" + }, + { + "format": "string", + "iid": 5, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "00000030-0000-1000-8000-0026BB765291", + "value": "EUCP03190xxxxx48" + }, + { + "format": "bool", + "iid": 6, + "perms": [ + "pw" + ], + "type": "00000014-0000-1000-8000-0026BB765291" + }, + { + "format": "string", + "iid": 37, + "perms": [ + "pr" + ], + "type": "00000052-0000-1000-8000-0026BB765291", + "value": "2.3.7" + } + ], + "iid": 1, + "stype": "accessory-information", + "type": "0000003E-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "ev": false, + "format": "bool", + "iid": 8, + "perms": [ + "pr", + "pw", + "ev" + ], + "type": "00000025-0000-1000-8000-0026BB765291", + "value": false + }, + { + "ev": false, + "format": "bool", + "iid": 9, + "perms": [ + "pr", + "ev" + ], + "type": "00000026-0000-1000-8000-0026BB765291", + "value": true + }, + { + "format": "string", + "iid": 10, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "00000023-0000-1000-8000-0026BB765291", + "value": "outlet" + } + ], + "iid": 7, + "primary": true, + "stype": "outlet", + "type": "00000047-0000-1000-8000-0026BB765291" + }, + { + "characteristics": [ + { + "description": "TIMER_SETTINGS", + "format": "tlv8", + "iid": 12, + "perms": [ + "pr", + "pw" + ], + "type": "4AAAF942-0DEC-11E5-B939-0800200C9A66", + "value": "AHAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + } + ], + "iid": 11, + "stype": "Unknown Service: 4AAAF940-0DEC-11E5-B939-0800200C9A66", + "type": "4AAAF940-0DEC-11E5-B939-0800200C9A66" + }, + { + "characteristics": [ + { + "description": "FW Upgrade supported types", + "format": "string", + "iid": 14, + "perms": [ + "pr", + "hd" + ], + "type": "151909D2-3802-11E4-916C-0800200C9A66", + "value": "url,data" + }, + { + "description": "FW Upgrade URL", + "format": "string", + "iid": 15, + "maxLen": 256, + "perms": [ + "pw", + "hd" + ], + "type": "151909D1-3802-11E4-916C-0800200C9A66" + }, + { + "description": "FW Upgrade Status", + "ev": false, + "format": "int", + "iid": 16, + "perms": [ + "pr", + "ev", + "hd" + ], + "type": "151909D6-3802-11E4-916C-0800200C9A66", + "value": 0 + }, + { + "description": "FW Upgrade Data", + "format": "data", + "iid": 17, + "perms": [ + "pw", + "hd" + ], + "type": "151909D7-3802-11E4-916C-0800200C9A66" + } + ], + "hidden": true, + "iid": 13, + "stype": "Unknown Service: 151909D0-3802-11E4-916C-0800200C9A66", + "type": "151909D0-3802-11E4-916C-0800200C9A66" + }, + { + "characteristics": [ + { + "description": "Timezone", + "format": "int", + "iid": 19, + "perms": [ + "pr", + "pw" + ], + "type": "151909D5-3802-11E4-916C-0800200C9A66", + "value": 0 + }, + { + "description": "Time value since Epoch", + "format": "int", + "iid": 20, + "perms": [ + "pr", + "pw" + ], + "type": "151909D4-3802-11E4-916C-0800200C9A66", + "value": 1570358601 + } + ], + "iid": 18, + "stype": "Unknown Service: 151909D3-3802-11E4-916C-0800200C9A66", + "type": "151909D3-3802-11E4-916C-0800200C9A66" + }, + { + "characteristics": [ + { + "description": "1 REALTIME_ENERGY", + "ev": false, + "format": "float", + "iid": 22, + "perms": [ + "pr", + "ev" + ], + "type": "4AAAF931-0DEC-11E5-B939-0800200C9A66", + "value": 5 + }, + { + "description": "2 CURRENT_HOUR_DATA", + "ev": false, + "format": "float", + "iid": 23, + "perms": [ + "pr", + "ev" + ], + "type": "4AAAF932-0DEC-11E5-B939-0800200C9A66", + "value": 0 + }, + { + "description": "3 HOUR_DATA_TODAY", + "format": "tlv8", + "iid": 24, + "perms": [ + "pr" + ], + "type": "4AAAF933-0DEC-11E5-B939-0800200C9A66", + "value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + }, + { + "description": "4 HOUR_DATA_YESTERDAY", + "format": "tlv8", + "iid": 25, + "perms": [ + "pr" + ], + "type": "4AAAF934-0DEC-11E5-B939-0800200C9A66", + "value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + }, + { + "description": "5 HOUR_DATA_2_DAYS_BEFORE", + "format": "tlv8", + "iid": 26, + "perms": [ + "pr" + ], + "type": "4AAAF935-0DEC-11E5-B939-0800200C9A66", + "value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + }, + { + "description": "6 HOUR_DATA_3_DAYS_BEFORE", + "format": "tlv8", + "iid": 27, + "perms": [ + "pr" + ], + "type": "4AAAF936-0DEC-11E5-B939-0800200C9A66", + "value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + }, + { + "description": "7 HOUR_DATA_4_DAYS_BEFORE", + "format": "tlv8", + "iid": 28, + "perms": [ + "pr" + ], + "type": "4AAAF937-0DEC-11E5-B939-0800200C9A66", + "value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + }, + { + "description": "8 HOUR_DATA_5_DAYS_BEFORE", + "format": "tlv8", + "iid": 29, + "perms": [ + "pr" + ], + "type": "4AAAF938-0DEC-11E5-B939-0800200C9A66", + "value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + }, + { + "description": "9 HOUR_DATA_6_DAYS_BEFORE", + "format": "tlv8", + "iid": 30, + "perms": [ + "pr" + ], + "type": "4AAAF939-0DEC-11E5-B939-0800200C9A66", + "value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + }, + { + "description": "10 HOUR_DATA_7_DAYS_BEFORE", + "format": "tlv8", + "iid": 31, + "perms": [ + "pr" + ], + "type": "4AAAF93A-0DEC-11E5-B939-0800200C9A66", + "value": "AGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + }, + { + "description": "11 DAY_DATA_THIS_MONTH", + "format": "tlv8", + "iid": 32, + "perms": [ + "pr" + ], + "type": "4AAAF93B-0DEC-11E5-B939-0800200C9A66", + "value": "AHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + }, + { + "description": "12 DAY_DATA_LAST_MONTH", + "format": "tlv8", + "iid": 33, + "perms": [ + "pr" + ], + "type": "4AAAF93C-0DEC-11E5-B939-0800200C9A66", + "value": "AHwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + }, + { + "description": "13 MONTH_DATA_THIS_YEAR", + "format": "tlv8", + "iid": 34, + "perms": [ + "pr" + ], + "type": "4AAAF93D-0DEC-11E5-B939-0800200C9A66", + "value": "ADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + }, + { + "description": "14 MONTH_DATA_LAST_YEAR", + "format": "tlv8", + "iid": 35, + "perms": [ + "pr" + ], + "type": "4AAAF93E-0DEC-11E5-B939-0800200C9A66", + "value": "ADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + }, + { + "description": "15 RUNNING_TIME", + "ev": false, + "format": "int", + "iid": 36, + "perms": [ + "pr", + "ev" + ], + "type": "4AAAF93F-0DEC-11E5-B939-0800200C9A66", + "value": 0 + } + ], + "iid": 21, + "stype": "Unknown Service: 4AAAF930-0DEC-11E5-B939-0800200C9A66", + "type": "4AAAF930-0DEC-11E5-B939-0800200C9A66" + }, + { + "characteristics": [ + { + "format": "string", + "iid": 39, + "maxLen": 64, + "perms": [ + "pr" + ], + "type": "00000037-0000-1000-8000-0026BB765291", + "value": "1.1.0" + } + ], + "iid": 38, + "stype": "service", + "type": "000000A2-0000-1000-8000-0026BB765291" + } + ] + } +] \ No newline at end of file diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json index beb7e42400f..4579fad30ba 100644 --- a/tests/fixtures/homematicip_cloud.json +++ b/tests/fixtures/homematicip_cloud.json @@ -14,6 +14,255 @@ } }, "devices": { + "3014F7110000RAIN_SENSOR": { + "availableFirmwareVersion": "1.0.18", + "connectionType": "HMIP_RF", + "firmwareVersion": "1.0.18", + "firmwareVersionInteger": 65554, + "functionalChannels": { + "0": { + "busConfigMismatch": null, + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F7110000RAIN_SENSOR", + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + "00000000-0000-0000-0000-000000000042" + ], + "index": 0, + "label": "", + "lowBat": null, + "multicastRoutingEnabled": false, + "powerShortCircuit": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -91, + "rssiPeerValue": null, + "shortCircuitDataLine": null, + "supportedOptionalFeatures": { + "IFeatureBusConfigMismatch": false, + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceIdentify": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDevicePowerFailure": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false, + "IFeatureMulticastRouter": false, + "IFeaturePowerShortCircuit": false, + "IFeatureRssiValue": true, + "IFeatureShortCircuitDataLine": false, + "IOptionalFeatureDutyCycle": true, + "IOptionalFeatureLowBat": false + }, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "deviceId": "3014F7110000RAIN_SENSOR", + "functionalChannelType": "RAIN_DETECTION_CHANNEL", + "groupIndex": 1, + "groups": [ + "00000000-0000-0000-0000-000000000043" + ], + "index": 1, + "label": "", + "rainSensorSensitivity": 50.0, + "raining": true + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110000RAIN_SENSOR", + "label": "Regensensor", + "lastStatusUpdate": 1610893608747, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 412, + "modelType": "HmIP-SRD", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110000RAIN_SENSOR", + "type": "RAIN_SENSOR", + "updateState": "UP_TO_DATE" + }, + "3014F7110DIN_RAIL_SWITCH": { + "availableFirmwareVersion": "1.6.0", + "connectionType": "HMIP_RF", + "firmwareVersion": "1.6.0", + "firmwareVersionInteger": 67072, + "functionalChannels": { + "0": { + "busConfigMismatch": null, + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": null, + "deviceId": "3014F7110DIN_RAIL_SWITCH", + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "dutyCycle": null, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + ], + "index": 0, + "label": "", + "lowBat": null, + "multicastRoutingEnabled": false, + "powerShortCircuit": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": null, + "rssiPeerValue": null, + "shortCircuitDataLine": null, + "supportedOptionalFeatures": { + "IFeatureBusConfigMismatch": false, + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceIdentify": true, + "IFeatureDeviceOverheated": true, + "IFeatureDeviceOverloaded": false, + "IFeatureDevicePowerFailure": true, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false, + "IFeatureMulticastRouter": false, + "IFeaturePowerShortCircuit": false, + "IFeatureRssiValue": true, + "IFeatureShortCircuitDataLine": false, + "IOptionalFeatureDutyCycle": true, + "IOptionalFeatureLowBat": false + }, + "temperatureOutOfRange": false, + "unreach": null + }, + "1": { + "binaryBehaviorType": "NORMALLY_CLOSE", + "deviceId": "3014F7110DIN_RAIL_SWITCH", + "functionalChannelType": "MULTI_MODE_INPUT_SWITCH_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 1, + "label": "OUT (1)", + "multiModeInputMode": "KEY_BEHAVIOR", + "on": null, + "profileMode": null, + "userDesiredProfileMode": "AUTOMATIC" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F7110DIN_RAIL_SWITCH", + "label": "Schaltaktor f\u00fcr Hutschienenmontage \u2013 1-fach", + "lastStatusUpdate": 0, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 422, + "modelType": "HmIP-DRSI1", + "oem": "eQ-3", + "permanentlyReachable": true, + "serializedGlobalTradeItemNumber": "3014F7110DIN_RAIL_SWITCH", + "type": "DIN_RAIL_SWITCH", + "updateState": "UP_TO_DATE" + }, + "3014F711PUSH_BUTTON_FLAT": { + "availableFirmwareVersion": "2.2.8", + "connectionType": "HMIP_RF", + "firmwareVersion": "2.2.8", + "firmwareVersionInteger": 131592, + "functionalChannels": { + "0": { + "busConfigMismatch": null, + "coProFaulty": false, + "coProRestartNeeded": false, + "coProUpdateFailure": false, + "configPending": false, + "deviceId": "3014F711PUSH_BUTTON_FLAT", + "deviceOverheated": false, + "deviceOverloaded": false, + "devicePowerFailureDetected": false, + "deviceUndervoltage": false, + "dutyCycle": false, + "functionalChannelType": "DEVICE_BASE", + "groupIndex": 0, + "groups": [ + ], + "index": 0, + "label": "", + "lowBat": false, + "multicastRoutingEnabled": false, + "powerShortCircuit": null, + "routerModuleEnabled": false, + "routerModuleSupported": false, + "rssiDeviceValue": -70, + "rssiPeerValue": null, + "shortCircuitDataLine": null, + "supportedOptionalFeatures": { + "IFeatureBusConfigMismatch": false, + "IFeatureDeviceCoProError": false, + "IFeatureDeviceCoProRestart": false, + "IFeatureDeviceCoProUpdate": false, + "IFeatureDeviceIdentify": false, + "IFeatureDeviceOverheated": false, + "IFeatureDeviceOverloaded": false, + "IFeatureDevicePowerFailure": false, + "IFeatureDeviceTemperatureOutOfRange": false, + "IFeatureDeviceUndervoltage": false, + "IFeatureMulticastRouter": false, + "IFeaturePowerShortCircuit": false, + "IFeatureRssiValue": true, + "IFeatureShortCircuitDataLine": false, + "IOptionalFeatureDutyCycle": true, + "IOptionalFeatureLowBat": true + }, + "temperatureOutOfRange": false, + "unreach": false + }, + "1": { + "deviceId": "3014F711PUSH_BUTTON_FLAT", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 1, + "label": "" + }, + "2": { + "deviceId": "3014F711PUSH_BUTTON_FLAT", + "functionalChannelType": "SINGLE_KEY_CHANNEL", + "groupIndex": 1, + "groups": [ + ], + "index": 2, + "label": "" + } + }, + "homeId": "00000000-0000-0000-0000-000000000001", + "id": "3014F711PUSH_BUTTON_FLAT", + "label": "Wandtaster", + "lastStatusUpdate": 1610331234765, + "liveUpdateState": "LIVE_UPDATE_NOT_SUPPORTED", + "manufacturerCode": 1, + "modelId": 431, + "modelType": "HmIP-WRCC2", + "oem": "eQ-3", + "permanentlyReachable": false, + "serializedGlobalTradeItemNumber": "3014F711PUSH_BUTTON_FLAT", + "type": "PUSH_BUTTON_FLAT", + "updateState": "UP_TO_DATE" + }, "3014F711A000000BAD0CAAAA": { "availableFirmwareVersion": "2.2.18", "connectionType": "HMIP_LAN", @@ -1172,7 +1421,6 @@ "3014F7110000000HmIPFSI16": { "availableFirmwareVersion": "0.0.0", "connectionType": "HMIP_RF", - "connectionType": "HMIP_RF", "firmwareVersion": "1.16.2", "firmwareVersionInteger": 69634, "functionalChannels": { diff --git a/tests/fixtures/ozw/migration_fixture.csv b/tests/fixtures/ozw/migration_fixture.csv new file mode 100644 index 00000000000..92b68f448f6 --- /dev/null +++ b/tests/fixtures/ozw/migration_fixture.csv @@ -0,0 +1,9 @@ +OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1008", "OZWDeamon_Version": "0.1", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1579566933, "ManufacturerSpecificDBReady": true, "homeID": 3245146787, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 3.95", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} +OpenZWave/1/node/32/,{ "NodeID": 32, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0208:0005:0101", "ZWAProductURL": "", "ProductPic": "images/hank/hkzw-so01-smartplug.png", "Description": "fixture description", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "Smart Plug", "ProductPicBase64": "iVBORggg==" }, "Event": "nodeQueriesComplete", "TimeStamp": 1579566933, "NodeManufacturerName": "HANK Electronics Ltd", "NodeProductName": "HKZW-SO01 Smart Plug", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Binary Switch", "NodeGeneric": 16, "NodeSpecificString": "Binary Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x0208", "NodeProductType": "0x0101", "NodeProductID": "0x0005", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "On/Off Power Switch", "NodeDeviceType": 1792, "NodeRole": 5, "NodeRoleString": "Always On Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 33, 36, 37, 39 ]} +OpenZWave/1/node/32/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/50/,{ "Instance": 1, "CommandClassId": 50, "CommandClass": "COMMAND_CLASS_METER", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/50/value/562950495305746/,{ "Label": "Electric - W", "Value": 0.0, "Units": "W", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 2, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 562950495305746, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/,{ "NodeID": 36, "NodeQueryStage": "CacheLoad", "isListening": false, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0086:007A:0102", "ZWAProductURL": "", "ProductPic": "images/aeotec/zw122.png", "Description": "fixture description", "WakeupHelp": "Pressing the Action Button once will trigger sending the Wake up notification command. If press and hold the Z-Wave button for 3 seconds, the Water Sensor will wake up for 10 minutes.", "ProductSupportURL": "", "Frequency": "", "Name": "Water Sensor 6", "ProductPicBase64": "kSuQmCC" }, "Event": "nodeNaming", "TimeStamp": 1579566891, "NodeManufacturerName": "AEON Labs", "NodeProductName": "ZW122 Water Sensor 6", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Notification Sensor", "NodeGeneric": 7, "NodeSpecificString": "Notification Sensor", "NodeSpecific": 1, "NodeManufacturerID": "0x0086", "NodeProductType": "0x0102", "NodeProductID": "0x007a", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 4} +OpenZWave/1/node/36/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/128/,{ "Instance": 1, "CommandClassId": 128, "CommandClass": "COMMAND_CLASS_BATTERY", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/128/value/610271249/,{ "Label": "Battery Level", "Value": 100, "Units": "%", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BATTERY", "Index": 0, "Node": 36, "Genre": "User", "Help": "Current Battery Level", "ValueIDKey": 610271249, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} diff --git a/tests/fixtures/plex/album.xml b/tests/fixtures/plex/album.xml new file mode 100644 index 00000000000..380149cf5ac --- /dev/null +++ b/tests/fixtures/plex/album.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tests/fixtures/plex/artist_albums.xml b/tests/fixtures/plex/artist_albums.xml new file mode 100644 index 00000000000..b1c8d1afb89 --- /dev/null +++ b/tests/fixtures/plex/artist_albums.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tests/fixtures/plex/children_20.xml b/tests/fixtures/plex/children_20.xml new file mode 100644 index 00000000000..6f433fff9a8 --- /dev/null +++ b/tests/fixtures/plex/children_20.xml @@ -0,0 +1,11 @@ + diff --git a/tests/fixtures/plex/children_200.xml b/tests/fixtures/plex/children_200.xml new file mode 100644 index 00000000000..e1ff4934651 --- /dev/null +++ b/tests/fixtures/plex/children_200.xml @@ -0,0 +1 @@ + diff --git a/tests/fixtures/plex/children_30.xml b/tests/fixtures/plex/children_30.xml new file mode 100644 index 00000000000..bf87607f0b0 --- /dev/null +++ b/tests/fixtures/plex/children_30.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tests/fixtures/plex/children_300.xml b/tests/fixtures/plex/children_300.xml new file mode 100644 index 00000000000..b1c8d1afb89 --- /dev/null +++ b/tests/fixtures/plex/children_300.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tests/fixtures/plex/empty_library.xml b/tests/fixtures/plex/empty_library.xml new file mode 100644 index 00000000000..853d3b9791f --- /dev/null +++ b/tests/fixtures/plex/empty_library.xml @@ -0,0 +1 @@ + diff --git a/tests/fixtures/plex/empty_payload.xml b/tests/fixtures/plex/empty_payload.xml new file mode 100644 index 00000000000..89bcdba2d58 --- /dev/null +++ b/tests/fixtures/plex/empty_payload.xml @@ -0,0 +1 @@ + diff --git a/tests/fixtures/plex/grandchildren_300.xml b/tests/fixtures/plex/grandchildren_300.xml new file mode 100644 index 00000000000..2c9741e2c1b --- /dev/null +++ b/tests/fixtures/plex/grandchildren_300.xml @@ -0,0 +1 @@ + diff --git a/tests/fixtures/plex/library.xml b/tests/fixtures/plex/library.xml new file mode 100644 index 00000000000..4d6ec69990b --- /dev/null +++ b/tests/fixtures/plex/library.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/tests/fixtures/plex/library_movies_all.xml b/tests/fixtures/plex/library_movies_all.xml new file mode 100644 index 00000000000..cd194040b37 --- /dev/null +++ b/tests/fixtures/plex/library_movies_all.xml @@ -0,0 +1,51 @@ + diff --git a/tests/fixtures/plex/library_movies_sort.xml b/tests/fixtures/plex/library_movies_sort.xml new file mode 100644 index 00000000000..052eac3590a --- /dev/null +++ b/tests/fixtures/plex/library_movies_sort.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/tests/fixtures/plex/library_music_all.xml b/tests/fixtures/plex/library_music_all.xml new file mode 100644 index 00000000000..6676817780d --- /dev/null +++ b/tests/fixtures/plex/library_music_all.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/tests/fixtures/plex/library_music_sort.xml b/tests/fixtures/plex/library_music_sort.xml new file mode 100644 index 00000000000..3a516a2a2f8 --- /dev/null +++ b/tests/fixtures/plex/library_music_sort.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/tests/fixtures/plex/library_sections.xml b/tests/fixtures/plex/library_sections.xml new file mode 100644 index 00000000000..954af4b6928 --- /dev/null +++ b/tests/fixtures/plex/library_sections.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/tests/fixtures/plex/library_tvshows_all.xml b/tests/fixtures/plex/library_tvshows_all.xml new file mode 100644 index 00000000000..e734d396ca2 --- /dev/null +++ b/tests/fixtures/plex/library_tvshows_all.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/tests/fixtures/plex/library_tvshows_sort.xml b/tests/fixtures/plex/library_tvshows_sort.xml new file mode 100644 index 00000000000..63df4738a24 --- /dev/null +++ b/tests/fixtures/plex/library_tvshows_sort.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/tests/fixtures/plex/media_1.xml b/tests/fixtures/plex/media_1.xml new file mode 100644 index 00000000000..838afb2959c --- /dev/null +++ b/tests/fixtures/plex/media_1.xml @@ -0,0 +1,11 @@ + diff --git a/tests/fixtures/plex/media_100.xml b/tests/fixtures/plex/media_100.xml new file mode 100644 index 00000000000..88ad7048fc0 --- /dev/null +++ b/tests/fixtures/plex/media_100.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/tests/fixtures/plex/media_200.xml b/tests/fixtures/plex/media_200.xml new file mode 100644 index 00000000000..380149cf5ac --- /dev/null +++ b/tests/fixtures/plex/media_200.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tests/fixtures/plex/media_30.xml b/tests/fixtures/plex/media_30.xml new file mode 100644 index 00000000000..14a69adc0c7 --- /dev/null +++ b/tests/fixtures/plex/media_30.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/tests/fixtures/plex/player_plexweb_resources.xml b/tests/fixtures/plex/player_plexweb_resources.xml new file mode 100644 index 00000000000..f3a2e31335a --- /dev/null +++ b/tests/fixtures/plex/player_plexweb_resources.xml @@ -0,0 +1 @@ + diff --git a/tests/fixtures/plex/playlist_500.xml b/tests/fixtures/plex/playlist_500.xml new file mode 100644 index 00000000000..d1d008549e8 --- /dev/null +++ b/tests/fixtures/plex/playlist_500.xml @@ -0,0 +1,11 @@ + diff --git a/tests/fixtures/plex/playlists.xml b/tests/fixtures/plex/playlists.xml new file mode 100644 index 00000000000..bc0dd69905e --- /dev/null +++ b/tests/fixtures/plex/playlists.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/tests/fixtures/plex/playqueue_1234.xml b/tests/fixtures/plex/playqueue_1234.xml new file mode 100644 index 00000000000..837c2ffbc3c --- /dev/null +++ b/tests/fixtures/plex/playqueue_1234.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/tests/fixtures/plex/playqueue_created.xml b/tests/fixtures/plex/playqueue_created.xml new file mode 100644 index 00000000000..72a274ca7b9 --- /dev/null +++ b/tests/fixtures/plex/playqueue_created.xml @@ -0,0 +1 @@ + diff --git a/tests/fixtures/plex/plex_server_accounts.xml b/tests/fixtures/plex/plex_server_accounts.xml new file mode 100644 index 00000000000..22b92d89c4a --- /dev/null +++ b/tests/fixtures/plex/plex_server_accounts.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/tests/fixtures/plex/plex_server_base.xml b/tests/fixtures/plex/plex_server_base.xml new file mode 100644 index 00000000000..da983d2f356 --- /dev/null +++ b/tests/fixtures/plex/plex_server_base.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/fixtures/plex/plex_server_clients.xml b/tests/fixtures/plex/plex_server_clients.xml new file mode 100644 index 00000000000..c7f6180e9c3 --- /dev/null +++ b/tests/fixtures/plex/plex_server_clients.xml @@ -0,0 +1,3 @@ + + + diff --git a/tests/fixtures/plex/plextv_account.xml b/tests/fixtures/plex/plextv_account.xml new file mode 100644 index 00000000000..32d6eec7c2d --- /dev/null +++ b/tests/fixtures/plex/plextv_account.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + testuser + testuser@email.com + 2000-01-01 12:34:56 UTC + faketoken + diff --git a/tests/fixtures/plex/plextv_resources_base.xml b/tests/fixtures/plex/plextv_resources_base.xml new file mode 100644 index 00000000000..41e61711d36 --- /dev/null +++ b/tests/fixtures/plex/plextv_resources_base.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/tests/fixtures/plex/security_token.xml b/tests/fixtures/plex/security_token.xml new file mode 100644 index 00000000000..1d7bde66fa6 --- /dev/null +++ b/tests/fixtures/plex/security_token.xml @@ -0,0 +1 @@ + diff --git a/tests/fixtures/plex/session_base.xml b/tests/fixtures/plex/session_base.xml new file mode 100644 index 00000000000..e7451e93af4 --- /dev/null +++ b/tests/fixtures/plex/session_base.xml @@ -0,0 +1,11 @@ + diff --git a/tests/fixtures/plex/session_photo.xml b/tests/fixtures/plex/session_photo.xml new file mode 100644 index 00000000000..952875e525e --- /dev/null +++ b/tests/fixtures/plex/session_photo.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/tests/fixtures/plex/session_plexweb.xml b/tests/fixtures/plex/session_plexweb.xml new file mode 100644 index 00000000000..40597d7b701 --- /dev/null +++ b/tests/fixtures/plex/session_plexweb.xml @@ -0,0 +1,11 @@ + diff --git a/tests/fixtures/plex/show_seasons.xml b/tests/fixtures/plex/show_seasons.xml new file mode 100644 index 00000000000..bf87607f0b0 --- /dev/null +++ b/tests/fixtures/plex/show_seasons.xml @@ -0,0 +1,4 @@ + + + + diff --git a/tests/fixtures/plex/sonos_resources.xml b/tests/fixtures/plex/sonos_resources.xml new file mode 100644 index 00000000000..1cf8f276822 --- /dev/null +++ b/tests/fixtures/plex/sonos_resources.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_all_devices.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_all_devices.json index bcaf40b4196..5a3492a3c6b 100644 --- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_all_devices.json +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_all_devices.json @@ -1 +1 @@ -{"df4a4a8169904cdb9c03d61a21f42140": {"name": "Zone Lisa Bios", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "12493538af164a409c6a1c79e38afe1c"}, "b310b72a0e354bfab43089919b9a88bf": {"name": "Floor kraan", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "c50f167537524366a5af7aa3942feb1e"}, "a2c3583e0a6349358998b760cea82d2a": {"name": "Bios Cv Thermostatic Radiator ", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "12493538af164a409c6a1c79e38afe1c"}, "b59bcebaf94b499ea7d46e4a66fb62d8": {"name": "Zone Lisa WK", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "c50f167537524366a5af7aa3942feb1e"}, "fe799307f1624099878210aa0b9f1475": {"name": "Adam", "types": {"py/set": ["home", "temperature", "thermostat"]}, "class": "gateway", "location": "1f9dcf83fd4e4b66b72ff787957bfe5d"}, "d3da73bde12a47d5a6b8f9dad971f2ec": {"name": "Thermostatic Radiator Jessie", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "82fa13f017d240daa0d0ea1775420f24"}, "21f2b542c49845e6bb416884c55778d6": {"name": "Playstation Smart Plug", "types": {"py/set": ["plug", "power"]}, "class": "game_console", "location": "cd143c07248f491493cea0533bc3d669"}, "78d1126fc4c743db81b61c20e88342a7": {"name": "CV Pomp", "types": {"py/set": ["plug", "power"]}, "class": "central_heating_pump", "location": "c50f167537524366a5af7aa3942feb1e"}, "90986d591dcd426cae3ec3e8111ff730": {"name": "Adam", "types": {"py/set": ["home", "temperature", "thermostat"]}, "class": "heater_central", "location": "1f9dcf83fd4e4b66b72ff787957bfe5d"}, "cd0ddb54ef694e11ac18ed1cbce5dbbd": {"name": "NAS", "types": {"py/set": ["plug", "power"]}, "class": "vcr", "location": "cd143c07248f491493cea0533bc3d669"}, "4a810418d5394b3f82727340b91ba740": {"name": "USG Smart Plug", "types": {"py/set": ["plug", "power"]}, "class": "router", "location": "cd143c07248f491493cea0533bc3d669"}, "02cf28bfec924855854c544690a609ef": {"name": "NVR", "types": {"py/set": ["plug", "power"]}, "class": "vcr", "location": "cd143c07248f491493cea0533bc3d669"}, "a28f588dc4a049a483fd03a30361ad3a": {"name": "Fibaro HC2", "types": {"py/set": ["plug", "power"]}, "class": "settop", "location": "cd143c07248f491493cea0533bc3d669"}, "6a3bf693d05e48e0b460c815a4fdd09d": {"name": "Zone Thermostat Jessie", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "82fa13f017d240daa0d0ea1775420f24"}, "680423ff840043738f42cc7f1ff97a36": {"name": "Thermostatic Radiator Badkamer", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "08963fec7c53423ca5680aa4cb502c63"}, "f1fee6043d3642a9b0a65297455f008e": {"name": "Zone Thermostat Badkamer", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "08963fec7c53423ca5680aa4cb502c63"}, "675416a629f343c495449970e2ca37b5": {"name": "Ziggo Modem", "types": {"py/set": ["plug", "power"]}, "class": "router", "location": "cd143c07248f491493cea0533bc3d669"}, "e7693eb9582644e5b865dba8d4447cf1": {"name": "CV Kraan Garage", "types": {"py/set": ["thermostat"]}, "class": "thermostatic_radiator_valve", "location": "446ac08dd04d4eff8ac57489757b7314"}} \ No newline at end of file +{"df4a4a8169904cdb9c03d61a21f42140": {"name": "Zone Lisa Bios", "model": "Zone Thermostat", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "12493538af164a409c6a1c79e38afe1c"}, "b310b72a0e354bfab43089919b9a88bf": {"name": "Floor kraan", "model": "Thermostatic Radiator Valve", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "c50f167537524366a5af7aa3942feb1e"}, "a2c3583e0a6349358998b760cea82d2a": {"name": "Bios Cv Thermostatic Radiator ", "model": "Thermostatic Radiator Valve", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "12493538af164a409c6a1c79e38afe1c"}, "b59bcebaf94b499ea7d46e4a66fb62d8": {"name": "Zone Lisa WK", "model": "Zone Thermostat", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "c50f167537524366a5af7aa3942feb1e"}, "fe799307f1624099878210aa0b9f1475": {"name": "Adam", "model": "Smile Adam", "types": {"py/set": ["temperature", "thermostat", "home"]}, "class": "gateway", "location": "1f9dcf83fd4e4b66b72ff787957bfe5d"}, "d3da73bde12a47d5a6b8f9dad971f2ec": {"name": "Thermostatic Radiator Jessie", "model": "Thermostatic Radiator Valve", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "82fa13f017d240daa0d0ea1775420f24"}, "21f2b542c49845e6bb416884c55778d6": {"name": "Playstation Smart Plug", "model": "Plug", "types": {"py/set": ["plug", "power"]}, "class": "game_console", "location": "cd143c07248f491493cea0533bc3d669"}, "78d1126fc4c743db81b61c20e88342a7": {"name": "CV Pomp", "model": "Plug", "types": {"py/set": ["plug", "power"]}, "class": "central_heating_pump", "location": "c50f167537524366a5af7aa3942feb1e"}, "90986d591dcd426cae3ec3e8111ff730": {"name": "Adam", "model": "Heater Central", "types": {"py/set": ["temperature", "thermostat", "home"]}, "class": "heater_central", "location": "1f9dcf83fd4e4b66b72ff787957bfe5d"}, "cd0ddb54ef694e11ac18ed1cbce5dbbd": {"name": "NAS", "model": "Plug", "types": {"py/set": ["plug", "power"]}, "class": "vcr", "location": "cd143c07248f491493cea0533bc3d669"}, "4a810418d5394b3f82727340b91ba740": {"name": "USG Smart Plug", "model": "Plug", "types": {"py/set": ["plug", "power"]}, "class": "router", "location": "cd143c07248f491493cea0533bc3d669"}, "02cf28bfec924855854c544690a609ef": {"name": "NVR", "model": "Plug", "types": {"py/set": ["plug", "power"]}, "class": "vcr", "location": "cd143c07248f491493cea0533bc3d669"}, "a28f588dc4a049a483fd03a30361ad3a": {"name": "Fibaro HC2", "model": "Plug", "types": {"py/set": ["plug", "power"]}, "class": "settop", "location": "cd143c07248f491493cea0533bc3d669"}, "6a3bf693d05e48e0b460c815a4fdd09d": {"name": "Zone Thermostat Jessie", "model": "Zone Thermostat", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "82fa13f017d240daa0d0ea1775420f24"}, "680423ff840043738f42cc7f1ff97a36": {"name": "Thermostatic Radiator Badkamer", "model": "Thermostatic Radiator Valve", "types": {"py/set": ["thermostat"]}, "class": "thermo_sensor", "location": "08963fec7c53423ca5680aa4cb502c63"}, "f1fee6043d3642a9b0a65297455f008e": {"name": "Zone Thermostat Badkamer", "model": "Zone Thermostat", "types": {"py/set": ["thermostat"]}, "class": "zone_thermostat", "location": "08963fec7c53423ca5680aa4cb502c63"}, "675416a629f343c495449970e2ca37b5": {"name": "Ziggo Modem", "model": "Plug", "types": {"py/set": ["plug", "power"]}, "class": "router", "location": "cd143c07248f491493cea0533bc3d669"}, "e7693eb9582644e5b865dba8d4447cf1": {"name": "CV Kraan Garage", "model": "Thermostatic Radiator Valve", "types": {"py/set": ["thermostat"]}, "class": "thermostatic_radiator_valve", "location": "446ac08dd04d4eff8ac57489757b7314"}} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/680423ff840043738f42cc7f1ff97a36.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/680423ff840043738f42cc7f1ff97a36.json index 6754cf63d2d..3ea0a92387b 100644 --- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/680423ff840043738f42cc7f1ff97a36.json +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/680423ff840043738f42cc7f1ff97a36.json @@ -1 +1 @@ -{"temperature": 19.1, "setpoint": 14.0, "battery": 0.51, "temperature_difference": -0.4, "valve_position": 0.0} \ No newline at end of file +{"temperature": 19.1, "setpoint": 14.0, "battery": 51, "temperature_difference": -0.4, "valve_position": 0.0} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/6a3bf693d05e48e0b460c815a4fdd09d.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/6a3bf693d05e48e0b460c815a4fdd09d.json index 14d596fb315..2d8ace6fa3f 100644 --- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/6a3bf693d05e48e0b460c815a4fdd09d.json +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/6a3bf693d05e48e0b460c815a4fdd09d.json @@ -1 +1 @@ -{"temperature": 17.2, "setpoint": 15.0, "battery": 0.37, "active_preset": "asleep", "presets": {"home": [20.0, 22.0], "no_frost": [10.0, 30.0], "away": [12.0, 25.0], "vacation": [11.0, 28.0], "asleep": [16.0, 24.0]}, "schedule_temperature": 16.5, "available_schedules": ["CV Jessie"], "selected_schedule": "CV Jessie", "last_used": "CV Jessie"} \ No newline at end of file +{"temperature": 17.2, "setpoint": 15.0, "battery": 37, "active_preset": "asleep", "presets": {"home": [20.0, 22.0], "no_frost": [10.0, 30.0], "away": [12.0, 25.0], "vacation": [11.0, 28.0], "asleep": [16.0, 24.0]}, "schedule_temperature": 15.0, "available_schedules": ["CV Jessie"], "selected_schedule": "CV Jessie", "last_used": "CV Jessie"} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/90986d591dcd426cae3ec3e8111ff730.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/90986d591dcd426cae3ec3e8111ff730.json index 862a3159754..d2f2f82bdf6 100644 --- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/90986d591dcd426cae3ec3e8111ff730.json +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/90986d591dcd426cae3ec3e8111ff730.json @@ -1 +1 @@ -{"water_temperature": 70.0, "intended_boiler_temperature": 70.0, "modulation_level": 0.01, "heating_state": true} \ No newline at end of file +{"water_temperature": 70.0, "intended_boiler_temperature": 70.0, "modulation_level": 1, "heating_state": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a2c3583e0a6349358998b760cea82d2a.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a2c3583e0a6349358998b760cea82d2a.json index c3e1a35b292..3f01f47fc5c 100644 --- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a2c3583e0a6349358998b760cea82d2a.json +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/a2c3583e0a6349358998b760cea82d2a.json @@ -1 +1 @@ -{"temperature": 17.2, "setpoint": 13.0, "battery": 0.62, "temperature_difference": -0.2, "valve_position": 0.0} \ No newline at end of file +{"temperature": 17.2, "setpoint": 13.0, "battery": 62, "temperature_difference": -0.2, "valve_position": 0.0} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b310b72a0e354bfab43089919b9a88bf.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b310b72a0e354bfab43089919b9a88bf.json index 8478716dc7b..3a1c902932a 100644 --- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b310b72a0e354bfab43089919b9a88bf.json +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b310b72a0e354bfab43089919b9a88bf.json @@ -1 +1 @@ -{"temperature": 26.0, "setpoint": 21.5, "temperature_difference": 3.5, "valve_position": 1.0} \ No newline at end of file +{"temperature": 26.0, "setpoint": 21.5, "temperature_difference": 3.5, "valve_position": 100} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b59bcebaf94b499ea7d46e4a66fb62d8.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b59bcebaf94b499ea7d46e4a66fb62d8.json index 6d1a8d135a4..2b314f589b6 100644 --- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b59bcebaf94b499ea7d46e4a66fb62d8.json +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/b59bcebaf94b499ea7d46e4a66fb62d8.json @@ -1 +1 @@ -{"temperature": 20.9, "setpoint": 21.5, "battery": 0.34, "active_preset": "home", "presets": {"vacation": [15.0, 28.0], "asleep": [18.0, 24.0], "no_frost": [12.0, 30.0], "away": [17.0, 25.0], "home": [21.5, 22.0]}, "schedule_temperature": 21.5, "available_schedules": ["GF7 Woonkamer"], "selected_schedule": "GF7 Woonkamer", "last_used": "GF7 Woonkamer"} \ No newline at end of file +{"temperature": 20.9, "setpoint": 21.5, "battery": 34, "active_preset": "home", "presets": {"vacation": [15.0, 28.0], "asleep": [18.0, 24.0], "no_frost": [12.0, 30.0], "away": [17.0, 25.0], "home": [21.5, 22.0]}, "schedule_temperature": 21.5, "available_schedules": ["GF7 Woonkamer"], "selected_schedule": "GF7 Woonkamer", "last_used": "GF7 Woonkamer"} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/d3da73bde12a47d5a6b8f9dad971f2ec.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/d3da73bde12a47d5a6b8f9dad971f2ec.json index b5a26000c7f..3e061593953 100644 --- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/d3da73bde12a47d5a6b8f9dad971f2ec.json +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/d3da73bde12a47d5a6b8f9dad971f2ec.json @@ -1 +1 @@ -{"temperature": 17.1, "setpoint": 15.0, "battery": 0.62, "temperature_difference": 0.1, "valve_position": 0.0} \ No newline at end of file +{"temperature": 17.1, "setpoint": 15.0, "battery": 62, "temperature_difference": 0.1, "valve_position": 0.0} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/df4a4a8169904cdb9c03d61a21f42140.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/df4a4a8169904cdb9c03d61a21f42140.json index f27c382fc0b..88420a8a6bd 100644 --- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/df4a4a8169904cdb9c03d61a21f42140.json +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/df4a4a8169904cdb9c03d61a21f42140.json @@ -1 +1 @@ -{"temperature": 16.5, "setpoint": 13.0, "battery": 0.67, "active_preset": "away", "presets": {"home": [20.0, 22.0], "away": [12.0, 25.0], "vacation": [12.0, 28.0], "no_frost": [8.0, 30.0], "asleep": [15.0, 24.0]}, "schedule_temperature": null, "available_schedules": [], "selected_schedule": null, "last_used": null} \ No newline at end of file +{"temperature": 16.5, "setpoint": 13.0, "battery": 67, "active_preset": "away", "presets": {"home": [20.0, 22.0], "away": [12.0, 25.0], "vacation": [12.0, 28.0], "no_frost": [8.0, 30.0], "asleep": [15.0, 24.0]}, "schedule_temperature": null, "available_schedules": [], "selected_schedule": null, "last_used": null} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/e7693eb9582644e5b865dba8d4447cf1.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/e7693eb9582644e5b865dba8d4447cf1.json index 610c019b686..7e4532987b0 100644 --- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/e7693eb9582644e5b865dba8d4447cf1.json +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/e7693eb9582644e5b865dba8d4447cf1.json @@ -1 +1 @@ -{"temperature": 15.6, "setpoint": 5.5, "battery": 0.68, "temperature_difference": 0.0, "valve_position": 0.0, "active_preset": "no_frost", "presets": {"home": [20.0, 22.0], "asleep": [17.0, 24.0], "away": [15.0, 25.0], "vacation": [15.0, 28.0], "no_frost": [10.0, 30.0]}, "schedule_temperature": null, "available_schedules": [], "selected_schedule": null, "last_used": null} \ No newline at end of file +{"temperature": 15.6, "setpoint": 5.5, "battery": 68, "temperature_difference": 0.0, "valve_position": 0.0, "active_preset": "no_frost", "presets": {"home": [20.0, 22.0], "asleep": [17.0, 24.0], "away": [15.0, 25.0], "vacation": [15.0, 28.0], "no_frost": [10.0, 30.0]}, "schedule_temperature": null, "available_schedules": [], "selected_schedule": null, "last_used": null} \ No newline at end of file diff --git a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/f1fee6043d3642a9b0a65297455f008e.json b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/f1fee6043d3642a9b0a65297455f008e.json index c4b5769e6d1..0d6e19967dc 100644 --- a/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/f1fee6043d3642a9b0a65297455f008e.json +++ b/tests/fixtures/plugwise/adam_multiple_devices_per_zone/get_device_data/f1fee6043d3642a9b0a65297455f008e.json @@ -1 +1 @@ -{"temperature": 18.9, "setpoint": 14.0, "battery": 0.92, "active_preset": "away", "presets": {"asleep": [17.0, 24.0], "no_frost": [10.0, 30.0], "away": [14.0, 25.0], "home": [21.0, 22.0], "vacation": [12.0, 28.0]}, "schedule_temperature": 14.0, "available_schedules": ["Badkamer Schema"], "selected_schedule": "Badkamer Schema", "last_used": "Badkamer Schema"} \ No newline at end of file +{"temperature": 18.9, "setpoint": 14.0, "battery": 92, "active_preset": "away", "presets": {"asleep": [17.0, 24.0], "no_frost": [10.0, 30.0], "away": [14.0, 25.0], "home": [21.0, 22.0], "vacation": [12.0, 28.0]}, "schedule_temperature": 14.0, "available_schedules": ["Badkamer Schema"], "selected_schedule": "Badkamer Schema", "last_used": "Badkamer Schema"} \ No newline at end of file diff --git a/tests/fixtures/plugwise/anna_heatpump/get_all_devices.json b/tests/fixtures/plugwise/anna_heatpump/get_all_devices.json index 191f5b442b7..ea46cd68054 100644 --- a/tests/fixtures/plugwise/anna_heatpump/get_all_devices.json +++ b/tests/fixtures/plugwise/anna_heatpump/get_all_devices.json @@ -1 +1 @@ -{"1cbf783bb11e4a7c8a6843dee3a86927": {"name": "Anna", "types": {"py/set": ["home", "temperature", "thermostat"]}, "class": "heater_central", "location": "a57efe5f145f498c9be62a9b63626fbf"}, "015ae9ea3f964e668e490fa39da3870b": {"name": "Anna", "types": {"py/set": ["home", "temperature", "thermostat"]}, "class": "gateway", "location": "a57efe5f145f498c9be62a9b63626fbf"}, "3cb70739631c4d17a86b8b12e8a5161b": {"name": "Anna", "types": {"py/set": ["thermostat"]}, "class": "thermostat", "location": "c784ee9fdab44e1395b8dee7d7a497d5"}} \ No newline at end of file +{"1cbf783bb11e4a7c8a6843dee3a86927": {"name": "Anna", "model": "Heater Central", "types": {"py/set": ["temperature", "thermostat", "home"]}, "class": "heater_central", "location": "a57efe5f145f498c9be62a9b63626fbf"}, "015ae9ea3f964e668e490fa39da3870b": {"name": "Anna", "model": "Smile Anna", "types": {"py/set": ["temperature", "thermostat", "home"]}, "class": "gateway", "location": "a57efe5f145f498c9be62a9b63626fbf"}, "3cb70739631c4d17a86b8b12e8a5161b": {"name": "Anna", "model": "Thermostat", "types": {"py/set": ["thermostat"]}, "class": "thermostat", "location": "c784ee9fdab44e1395b8dee7d7a497d5"}} \ No newline at end of file diff --git a/tests/fixtures/plugwise/anna_heatpump/get_device_data/1cbf783bb11e4a7c8a6843dee3a86927.json b/tests/fixtures/plugwise/anna_heatpump/get_device_data/1cbf783bb11e4a7c8a6843dee3a86927.json index ddf807303a2..604b9388969 100644 --- a/tests/fixtures/plugwise/anna_heatpump/get_device_data/1cbf783bb11e4a7c8a6843dee3a86927.json +++ b/tests/fixtures/plugwise/anna_heatpump/get_device_data/1cbf783bb11e4a7c8a6843dee3a86927.json @@ -1 +1 @@ -{"water_temperature": 29.1, "dhw_state": false, "intended_boiler_temperature": 0.0, "heating_state": false, "modulation_level": 0.52, "return_temperature": 25.1, "compressor_state": true, "cooling_state": false, "slave_boiler_state": false, "flame_state": false, "water_pressure": 1.57, "outdoor_temperature": 18.0} \ No newline at end of file +{"water_temperature": 29.1, "dhw_state": false, "intended_boiler_temperature": 0.0, "heating_state": false, "modulation_level": 52, "return_temperature": 25.1, "compressor_state": true, "cooling_state": false, "slave_boiler_state": false, "flame_state": false, "water_pressure": 1.57, "outdoor_temperature": 18.0} \ No newline at end of file diff --git a/tests/fixtures/plugwise/anna_heatpump/get_device_data/3cb70739631c4d17a86b8b12e8a5161b.json b/tests/fixtures/plugwise/anna_heatpump/get_device_data/3cb70739631c4d17a86b8b12e8a5161b.json index 3177880705b..048cc0f77dc 100644 --- a/tests/fixtures/plugwise/anna_heatpump/get_device_data/3cb70739631c4d17a86b8b12e8a5161b.json +++ b/tests/fixtures/plugwise/anna_heatpump/get_device_data/3cb70739631c4d17a86b8b12e8a5161b.json @@ -1 +1 @@ -{"temperature": 23.3, "setpoint": 21.0, "active_preset": "home", "presets": {"no_frost": [10.0, 30.0], "home": [21.0, 22.0], "away": [20.0, 25.0], "asleep": [20.5, 24.0], "vacation": [17.0, 28.0]}, "schedule_temperature": null, "available_schedules": ["standaard"], "selected_schedule": "standaard", "last_used": "standaard", "illuminance": 86.0} \ No newline at end of file +{"temperature": 23.3, "setpoint": 21.0, "heating_state": false, "active_preset": "home", "presets": {"no_frost": [10.0, 30.0], "home": [21.0, 22.0], "away": [20.0, 25.0], "asleep": [20.5, 24.0], "vacation": [17.0, 28.0]}, "schedule_temperature": null, "available_schedules": ["standaard"], "selected_schedule": "standaard", "last_used": "standaard", "illuminance": 86.0} \ No newline at end of file diff --git a/tests/fixtures/plugwise/p1v3_full_option/get_all_devices.json b/tests/fixtures/plugwise/p1v3_full_option/get_all_devices.json index 1feb33dd630..a78f45ead8a 100644 --- a/tests/fixtures/plugwise/p1v3_full_option/get_all_devices.json +++ b/tests/fixtures/plugwise/p1v3_full_option/get_all_devices.json @@ -1 +1 @@ -{"e950c7d5e1ee407a858e2a8b5016c8b3": {"name": "P1", "types": {"py/set": ["home", "power"]}, "class": "gateway", "location": "cd3e822288064775a7c4afcdd70bdda2"}} \ No newline at end of file +{"e950c7d5e1ee407a858e2a8b5016c8b3": {"name": "P1", "model": "Smile P1", "types": {"py/set": ["home", "power"]}, "class": "gateway", "location": "cd3e822288064775a7c4afcdd70bdda2"}} \ No newline at end of file diff --git a/tests/fixtures/plugwise/p1v3_full_option/get_device_data/e950c7d5e1ee407a858e2a8b5016c8b3.json b/tests/fixtures/plugwise/p1v3_full_option/get_device_data/e950c7d5e1ee407a858e2a8b5016c8b3.json index fcbc1bbce33..eed9382a7e9 100644 --- a/tests/fixtures/plugwise/p1v3_full_option/get_device_data/e950c7d5e1ee407a858e2a8b5016c8b3.json +++ b/tests/fixtures/plugwise/p1v3_full_option/get_device_data/e950c7d5e1ee407a858e2a8b5016c8b3.json @@ -1 +1 @@ -{"net_electricity_point": -2761.0, "electricity_consumed_peak_point": 0.0, "electricity_consumed_off_peak_point": 0.0, "net_electricity_cumulative": 442972.0, "electricity_consumed_peak_cumulative": 442932.0, "electricity_consumed_off_peak_cumulative": 551090.0, "net_electricity_interval": 0.0, "electricity_consumed_peak_interval": 0.0, "electricity_consumed_off_peak_interval": 0.0, "electricity_produced_peak_point": 2761.0, "electricity_produced_off_peak_point": 0.0, "electricity_produced_peak_cumulative": 396559.0, "electricity_produced_off_peak_cumulative": 154491.0, "electricity_produced_peak_interval": 0.0, "electricity_produced_off_peak_interval": 0.0, "gas_consumed_cumulative": 584.85, "gas_consumed_interval": 0.0} \ No newline at end of file +{"net_electricity_point": -2761, "electricity_consumed_peak_point": 0, "electricity_consumed_off_peak_point": 0, "net_electricity_cumulative": 442.972, "electricity_consumed_peak_cumulative": 442.932, "electricity_consumed_off_peak_cumulative": 551.09, "net_electricity_interval": 0, "electricity_consumed_peak_interval": 0, "electricity_consumed_off_peak_interval": 0, "electricity_produced_peak_point": 2761, "electricity_produced_off_peak_point": 0, "electricity_produced_peak_cumulative": 396.559, "electricity_produced_off_peak_cumulative": 154.491, "electricity_produced_peak_interval": 0, "electricity_produced_off_peak_interval": 0, "gas_consumed_cumulative": 584.85, "gas_consumed_interval": 0.0} \ No newline at end of file diff --git a/tests/fixtures/plugwise/stretch_v31/get_all_devices.json b/tests/fixtures/plugwise/stretch_v31/get_all_devices.json index f40e902d5a9..dab74fb74a2 100644 --- a/tests/fixtures/plugwise/stretch_v31/get_all_devices.json +++ b/tests/fixtures/plugwise/stretch_v31/get_all_devices.json @@ -1 +1 @@ -{"cfe95cf3de1948c0b8955125bf754614": {"name": "Droger (52559)", "types": {"py/set": ["plug", "power"]}, "class": "dryer", "location": 0}, "aac7b735042c4832ac9ff33aae4f453b": {"name": "Vaatwasser (2a1ab)", "types": {"py/set": ["plug", "power"]}, "class": "dishwasher", "location": 0}, "5871317346d045bc9f6b987ef25ee638": {"name": "Boiler (1EB31)", "types": {"py/set": ["plug", "power"]}, "class": "water_heater_vessel", "location": 0}, "059e4d03c7a34d278add5c7a4a781d19": {"name": "Wasmachine (52AC1)", "types": {"py/set": ["plug", "power"]}, "class": "washingmachine", "location": 0}, "e1c884e7dede431dadee09506ec4f859": {"name": "Koelkast (92C4A)", "types": {"py/set": ["plug", "power"]}, "class": "refrigerator", "location": 0}, "d950b314e9d8499f968e6db8d82ef78c": {"name": "Stroomvreters", "types": {"py/set": ["switch_group"]}, "class": "switching", "members": [], "location": null}} \ No newline at end of file +{"5ca521ac179d468e91d772eeeb8a2117": {"name": "Oven (793F84)", "model": "Circle", "types": {"py/set": ["plug", "power"]}, "class": "zz_misc", "location": 0}, "5871317346d045bc9f6b987ef25ee638": {"name": "Boiler (1EB31)", "model": "Circle", "types": {"py/set": ["plug", "power"]}, "class": "water_heater_vessel", "location": 0}, "e1c884e7dede431dadee09506ec4f859": {"name": "Koelkast (92C4A)", "model": "Circle+", "types": {"py/set": ["plug", "power"]}, "class": "refrigerator", "location": 0}, "aac7b735042c4832ac9ff33aae4f453b": {"name": "Vaatwasser (2a1ab)", "model": "Circle", "types": {"py/set": ["plug", "power"]}, "class": "dishwasher", "location": 0}, "cfe95cf3de1948c0b8955125bf754614": {"name": "Droger (52559)", "model": "Circle", "types": {"py/set": ["plug", "power"]}, "class": "dryer", "location": 0}, "99f89d097be34fca88d8598c6dbc18ea": {"name": "Meterkast (787BFB)", "model": "Circle", "types": {"py/set": ["plug", "power"]}, "class": "router", "location": 0}, "059e4d03c7a34d278add5c7a4a781d19": {"name": "Wasmachine (52AC1)", "model": "Circle", "types": {"py/set": ["plug", "power"]}, "class": "washingmachine", "location": 0}, "e309b52ea5684cf1a22f30cf0cd15051": {"name": "Computer (788618)", "model": "Circle", "types": {"py/set": ["plug", "power"]}, "class": "computer_desktop", "location": 0}, "71e1944f2a944b26ad73323e399efef0": {"name": "Test", "model": "group_switch", "types": {"py/set": ["switch_group"]}, "class": "switching", "members": ["5ca521ac179d468e91d772eeeb8a2117"], "location": null}, "d950b314e9d8499f968e6db8d82ef78c": {"name": "Stroomvreters", "model": "group_switch", "types": {"py/set": ["switch_group"]}, "class": "report", "members": ["059e4d03c7a34d278add5c7a4a781d19", "5871317346d045bc9f6b987ef25ee638", "aac7b735042c4832ac9ff33aae4f453b", "cfe95cf3de1948c0b8955125bf754614", "e1c884e7dede431dadee09506ec4f859"], "location": null}, "d03738edfcc947f7b8f4573571d90d2d": {"name": "Schakel", "model": "group_switch", "types": {"py/set": ["switch_group"]}, "class": "switching", "members": ["059e4d03c7a34d278add5c7a4a781d19", "cfe95cf3de1948c0b8955125bf754614"], "location": null}} \ No newline at end of file diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/5871317346d045bc9f6b987ef25ee638.json b/tests/fixtures/plugwise/stretch_v31/get_device_data/5871317346d045bc9f6b987ef25ee638.json index 529c8b76d95..4a3e493b246 100644 --- a/tests/fixtures/plugwise/stretch_v31/get_device_data/5871317346d045bc9f6b987ef25ee638.json +++ b/tests/fixtures/plugwise/stretch_v31/get_device_data/5871317346d045bc9f6b987ef25ee638.json @@ -1 +1 @@ -{"electricity_consumed": 1.19, "electricity_produced": 0.0, "relay": true} \ No newline at end of file +{"electricity_consumed": 1.19, "electricity_consumed_interval": 0.0, "electricity_produced": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/5ca521ac179d468e91d772eeeb8a2117.json b/tests/fixtures/plugwise/stretch_v31/get_device_data/5ca521ac179d468e91d772eeeb8a2117.json new file mode 100644 index 00000000000..7325dff8271 --- /dev/null +++ b/tests/fixtures/plugwise/stretch_v31/get_device_data/5ca521ac179d468e91d772eeeb8a2117.json @@ -0,0 +1 @@ +{"electricity_consumed": 0.0, "electricity_consumed_interval": 0.0, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/71e1944f2a944b26ad73323e399efef0.json b/tests/fixtures/plugwise/stretch_v31/get_device_data/71e1944f2a944b26ad73323e399efef0.json new file mode 100644 index 00000000000..bbb8ac98c1c --- /dev/null +++ b/tests/fixtures/plugwise/stretch_v31/get_device_data/71e1944f2a944b26ad73323e399efef0.json @@ -0,0 +1 @@ +{"relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/99f89d097be34fca88d8598c6dbc18ea.json b/tests/fixtures/plugwise/stretch_v31/get_device_data/99f89d097be34fca88d8598c6dbc18ea.json new file mode 100644 index 00000000000..b0cab0e3f30 --- /dev/null +++ b/tests/fixtures/plugwise/stretch_v31/get_device_data/99f89d097be34fca88d8598c6dbc18ea.json @@ -0,0 +1 @@ +{"electricity_consumed": 27.6, "electricity_consumed_interval": 28.2, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/aac7b735042c4832ac9ff33aae4f453b.json b/tests/fixtures/plugwise/stretch_v31/get_device_data/aac7b735042c4832ac9ff33aae4f453b.json index 35ce04f51cf..e58bc4c6d6f 100644 --- a/tests/fixtures/plugwise/stretch_v31/get_device_data/aac7b735042c4832ac9ff33aae4f453b.json +++ b/tests/fixtures/plugwise/stretch_v31/get_device_data/aac7b735042c4832ac9ff33aae4f453b.json @@ -1 +1 @@ -{"electricity_consumed": 0.0, "electricity_produced": 0.0, "relay": true} \ No newline at end of file +{"electricity_consumed": 0.0, "electricity_consumed_interval": 0.71, "electricity_produced": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/cfe95cf3de1948c0b8955125bf754614.json b/tests/fixtures/plugwise/stretch_v31/get_device_data/cfe95cf3de1948c0b8955125bf754614.json index 42de4d3338b..b08f6d6093a 100644 --- a/tests/fixtures/plugwise/stretch_v31/get_device_data/cfe95cf3de1948c0b8955125bf754614.json +++ b/tests/fixtures/plugwise/stretch_v31/get_device_data/cfe95cf3de1948c0b8955125bf754614.json @@ -1 +1 @@ -{"electricity_consumed": 0.0, "electricity_consumed_interval": 1.06, "electricity_produced": 0.0, "relay": true} \ No newline at end of file +{"electricity_consumed": 0.0, "electricity_consumed_interval": 0.0, "electricity_produced": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/d03738edfcc947f7b8f4573571d90d2d.json b/tests/fixtures/plugwise/stretch_v31/get_device_data/d03738edfcc947f7b8f4573571d90d2d.json new file mode 100644 index 00000000000..bbb8ac98c1c --- /dev/null +++ b/tests/fixtures/plugwise/stretch_v31/get_device_data/d03738edfcc947f7b8f4573571d90d2d.json @@ -0,0 +1 @@ +{"relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/d950b314e9d8499f968e6db8d82ef78c.json b/tests/fixtures/plugwise/stretch_v31/get_device_data/d950b314e9d8499f968e6db8d82ef78c.json index de5baf4c9a6..bbb8ac98c1c 100644 --- a/tests/fixtures/plugwise/stretch_v31/get_device_data/d950b314e9d8499f968e6db8d82ef78c.json +++ b/tests/fixtures/plugwise/stretch_v31/get_device_data/d950b314e9d8499f968e6db8d82ef78c.json @@ -1 +1 @@ -{"relay": false} \ No newline at end of file +{"relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/e1c884e7dede431dadee09506ec4f859.json b/tests/fixtures/plugwise/stretch_v31/get_device_data/e1c884e7dede431dadee09506ec4f859.json index 1a7249b68d5..11ebae52f49 100644 --- a/tests/fixtures/plugwise/stretch_v31/get_device_data/e1c884e7dede431dadee09506ec4f859.json +++ b/tests/fixtures/plugwise/stretch_v31/get_device_data/e1c884e7dede431dadee09506ec4f859.json @@ -1 +1 @@ -{"electricity_consumed": 53.2, "electricity_produced": 0.0, "relay": true} \ No newline at end of file +{"electricity_consumed": 50.5, "electricity_consumed_interval": 0.08, "electricity_produced": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/stretch_v31/get_device_data/e309b52ea5684cf1a22f30cf0cd15051.json b/tests/fixtures/plugwise/stretch_v31/get_device_data/e309b52ea5684cf1a22f30cf0cd15051.json new file mode 100644 index 00000000000..456fb6744d2 --- /dev/null +++ b/tests/fixtures/plugwise/stretch_v31/get_device_data/e309b52ea5684cf1a22f30cf0cd15051.json @@ -0,0 +1 @@ +{"electricity_consumed": 156, "electricity_consumed_interval": 163, "electricity_produced": 0.0, "electricity_produced_interval": 0.0, "relay": true} \ No newline at end of file diff --git a/tests/fixtures/plugwise/stretch_v31/notifications.json b/tests/fixtures/plugwise/stretch_v31/notifications.json new file mode 100644 index 00000000000..9e26dfeeb6e --- /dev/null +++ b/tests/fixtures/plugwise/stretch_v31/notifications.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/tests/fixtures/tado/device_temp_offset.json b/tests/fixtures/tado/device_temp_offset.json new file mode 100644 index 00000000000..79e25745cbb --- /dev/null +++ b/tests/fixtures/tado/device_temp_offset.json @@ -0,0 +1 @@ +{"celsius": -1.0, "fahrenheit": -1.8} diff --git a/tests/fixtures/tado/device_wr1.json b/tests/fixtures/tado/device_wr1.json new file mode 100644 index 00000000000..676784aeba3 --- /dev/null +++ b/tests/fixtures/tado/device_wr1.json @@ -0,0 +1,20 @@ +{ + "deviceType" : "WR02", + "currentFwVersion" : "59.4", + "accessPointWiFi" : { + "ssid" : "tado8480" + }, + "characteristics" : { + "capabilities" : [ + "INSIDE_TEMPERATURE_MEASUREMENT", + "IDENTIFY" + ] + }, + "serialNo" : "WR1", + "commandTableUploadState" : "FINISHED", + "connectionState" : { + "timestamp" : "2020-03-23T18:30:07.377Z", + "value" : true + }, + "shortSerialNo" : "WR1" +} diff --git a/tests/fixtures/zwave_js/bulb_6_multi_color_state.json b/tests/fixtures/zwave_js/bulb_6_multi_color_state.json new file mode 100644 index 00000000000..b7c422121c9 --- /dev/null +++ b/tests/fixtures/zwave_js/bulb_6_multi_color_state.json @@ -0,0 +1,632 @@ +{ + "nodeId": 39, + "index": 0, + "installerIcon": 1536, + "userIcon": 1536, + "status": 4, + "ready": true, + "deviceClass": { + "basic": "Static Controller", + "generic": "Multilevel Switch", + "specific": "Multilevel Power Switch", + "mandatorySupportedCCs": [ + "Basic", + "Multilevel Switch", + "All Switch" + ], + "mandatoryControlCCs": [] + }, + "isListening": true, + "isFrequentListening": false, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": "unknown", + "version": 4, + "isBeaming": true, + "manufacturerId": 881, + "productId": 2, + "productType": 259, + "firmwareVersion": "2.0", + "zwavePlusVersion": 1, + "nodeType": 0, + "roleType": 5, + "deviceConfig": { + "manufacturerId": 881, + "manufacturer": "Aeotec Ltd.", + "label": "ZWA002", + "description": "Bulb 6 Multi-Color", + "devices": [ + { + "productType": "0x0003", + "productId": "0x0002" + }, + { + "productType": "0x0103", + "productId": "0x0002" + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + } + }, + "label": "ZWA002", + "neighbors": [ + 1, + 32 + ], + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 39, + "index": 0, + "installerIcon": 1536, + "userIcon": 1536 + } + ], + "values": [ + { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 99, + "label": "Target value" + } + }, + { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "duration", + "propertyName": "duration", + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Transition duration" + } + }, + { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "currentValue", + "propertyName": "currentValue", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 99, + "label": "Current value" + }, + "value": 0 + }, + { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "Up", + "propertyName": "Up", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { + "switchType": 2 + } + } + }, + { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "Down", + "propertyName": "Down", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { + "switchType": 2 + } + } + }, + { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "duration", + "propertyName": "duration", + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Transition duration" + } + }, + { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "currentColor", + "propertyKey": 0, + "propertyName": "currentColor", + "propertyKeyName": "Warm White", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Current value (Warm White)", + "description": "The current value of the Warm White color." + }, + "value": 255 + }, + { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "currentColor", + "propertyKey": 1, + "propertyName": "currentColor", + "propertyKeyName": "Cold White", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Current value (Cold White)", + "description": "The current value of the Cold White color." + }, + "value": 0 + }, + { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "currentColor", + "propertyKey": 2, + "propertyName": "currentColor", + "propertyKeyName": "Red", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Current value (Red)", + "description": "The current value of the Red color." + }, + "value": 0 + }, + { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "currentColor", + "propertyKey": 3, + "propertyName": "currentColor", + "propertyKeyName": "Green", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Current value (Green)", + "description": "The current value of the Green color." + }, + "value": 0 + }, + { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "currentColor", + "propertyKey": 4, + "propertyName": "currentColor", + "propertyKeyName": "Blue", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Current value (Blue)", + "description": "The current value of the Blue color." + }, + "value": 0 + }, + { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "targetColor", + "propertyKey": 0, + "propertyName": "targetColor", + "propertyKeyName": "Warm White", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 255, + "label": "Target value (Warm White)", + "description": "The target value of the Warm White color." + } + }, + { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "targetColor", + "propertyKey": 1, + "propertyName": "targetColor", + "propertyKeyName": "Cold White", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 255, + "label": "Target value (Cold White)", + "description": "The target value of the Cold White color." + } + }, + { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "targetColor", + "propertyKey": 2, + "propertyName": "targetColor", + "propertyKeyName": "Red", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 255, + "label": "Target value (Red)", + "description": "The target value of the Red color." + } + }, + { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "targetColor", + "propertyKey": 3, + "propertyName": "targetColor", + "propertyKeyName": "Green", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 255, + "label": "Target value (Green)", + "description": "The target value of the Green color." + } + }, + { + "commandClassName": "Color Switch", + "commandClass": 51, + "endpoint": 0, + "property": "targetColor", + "propertyKey": 4, + "propertyName": "targetColor", + "propertyKeyName": "Blue", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 255, + "label": "Target value (Blue)", + "description": "The target value of the Blue color." + } + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 1, + "propertyName": "Use custom mode for LED animations", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 2, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Disable", + "1": "Blink Colors in order mode", + "2": "Randomized blink color mode" + }, + "label": "Use custom mode for LED animations", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 2, + "propertyName": "Enable/Disable Strobe over Custom Color", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Disable", + "1": "Enable" + }, + "label": "Enable/Disable Strobe over Custom Color", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 3, + "propertyName": "Rate of change to next color in Custom Mode", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 5, + "max": 8640000, + "default": 50, + "format": 0, + "allowManualEntry": true, + "label": "Rate of change to next color in Custom Mode", + "isFromConfig": true + }, + "value": 50 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 16, + "propertyName": "Ramp rate when dimming using Multilevel Switch", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 20, + "format": 0, + "allowManualEntry": true, + "label": "Ramp rate when dimming using Multilevel Switch", + "isFromConfig": true + }, + "value": 20 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 80, + "propertyName": "Enable notifications", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Nothing", + "1": "Basic CC report" + }, + "label": "Enable notifications", + "description": "Enable notifications to associated devices (Group 1) when the state is changed", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 81, + "propertyName": "Adjust color component of Warm White", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 2700, + "max": 4999, + "default": 2700, + "format": 0, + "allowManualEntry": true, + "label": "Adjust color component of Warm White", + "isFromConfig": true + }, + "value": 2700 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 82, + "propertyName": "Adjust color component of Cold White", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 5000, + "max": 6500, + "default": 6500, + "format": 0, + "allowManualEntry": true, + "label": "Adjust color component of Cold White", + "isFromConfig": true + }, + "value": 6500 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 4, + "propertyName": "Set color that LED Bulb blinks in (Blink Mode)", + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 255, + "default": 1, + "format": 1, + "allowManualEntry": true, + "label": "Set color that LED Bulb blinks in (Blink Mode)", + "isFromConfig": true + } + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "manufacturerId", + "propertyName": "manufacturerId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 881 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productType", + "propertyName": "productType", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 259 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productId", + "propertyName": "productId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 2 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "libraryType", + "propertyName": "libraryType", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Libary type" + }, + "value": 3 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "protocolVersion", + "propertyName": "protocolVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.38" + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": [ + "2.0" + ] + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + } + ] +} diff --git a/tests/fixtures/zwave_js/chain_actuator_zws12_state.json b/tests/fixtures/zwave_js/chain_actuator_zws12_state.json new file mode 100644 index 00000000000..dbae35e04d0 --- /dev/null +++ b/tests/fixtures/zwave_js/chain_actuator_zws12_state.json @@ -0,0 +1,406 @@ +{ + "nodeId": 6, + "index": 0, + "installerIcon": 6656, + "userIcon": 6656, + "status": 4, + "ready": true, + "deviceClass": { + "basic": "Routing Slave", + "generic": "Multilevel Switch", + "specific": "Motor Control Class C", + "mandatorySupportedCCs": [ + "Basic", + "Multilevel Switch", + "Binary Switch", + "Manufacturer Specific", + "Version" + ], + "mandatoryControlCCs": [] + }, + "isListening": true, + "isFrequentListening": false, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": false, + "version": 4, + "isBeaming": true, + "manufacturerId": 133, + "productId": 273, + "productType": 2, + "firmwareVersion": "1.1", + "zwavePlusVersion": 1, + "nodeType": 0, + "roleType": 5, + "name": "ZWS 12\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", + "location": "UNKNOWN\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000", + "deviceConfig": { + "manufacturerId": 133, + "manufacturer": "Fakro", + "label": "ZWS12n", + "description": "Chain actuator - window opener", + "devices": [ + { "productType": "0x0002", "productId": "0x0011" }, + { "productType": "0x0002", "productId": "0x0111" } + ], + "firmwareVersion": { "min": "0.0", "max": "255.255" }, + "paramInformation": { "_map": {} } + }, + "label": "ZWS12n", + "neighbors": [1, 2], + "interviewAttempts": 1, + "endpoints": [ + { "nodeId": 6, "index": 0, "installerIcon": 6656, "userIcon": 6656 } + ], + "values": [ + { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 99, + "label": "Target value" + } + }, + { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "duration", + "propertyName": "duration", + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Transition duration" + } + }, + { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "currentValue", + "propertyName": "currentValue", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 99, + "label": "Current value" + }, + "value": 0 + }, + { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "Open", + "propertyName": "Open", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Open)", + "ccSpecific": { "switchType": 3 } + } + }, + { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "Close", + "propertyName": "Close", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Close)", + "ccSpecific": { "switchType": 3 } + } + }, + { + "commandClassName": "Binary Switch", + "commandClass": 37, + "endpoint": 0, + "property": "currentValue", + "propertyName": "currentValue", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value" + }, + "value": false + }, + { + "commandClassName": "Binary Switch", + "commandClass": 37, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value" + } + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "manufacturerId", + "propertyName": "manufacturerId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 133 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productType", + "propertyName": "productType", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 2 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productId", + "propertyName": "productId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 273 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "libraryType", + "propertyName": "libraryType", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "protocolVersion", + "propertyName": "protocolVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.33" + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["1.1"] + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 13, + "propertyName": "Last saved position", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 2, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Last saved position", + "description": "Set servomotor in previous position", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 15, + "propertyName": "Close after time", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 120, + "default": 120, + "format": 0, + "allowManualEntry": true, + "label": "Close after time", + "description": "Close after time min", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 7, + "propertyName": "Motor speed I", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 4, + "default": 2, + "format": 0, + "allowManualEntry": true, + "label": "Motor speed I", + "description": "Motor speed I", + "isFromConfig": true + } + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 8, + "propertyName": "1 Motor speed II (rain sensor)", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 4, + "default": 2, + "format": 0, + "allowManualEntry": true, + "label": "1 Motor speed II (rain sensor)", + "description": "1 Motor speed II (rain sensor)", + "isFromConfig": true + } + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 12, + "propertyName": "Callibrate", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 2, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Callibrate", + "description": "This parameter on/off callibration function", + "isFromConfig": true + } + }, + { + "commandClassName": "Notification", + "commandClass": 113, + "endpoint": 0, + "property": "Water Alarm", + "propertyKey": "Sensor status", + "propertyName": "Water Alarm", + "propertyKeyName": "Sensor status", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Sensor status", + "states": { "0": "idle", "2": "Water leak detected" }, + "ccSpecific": { "notificationType": 5 } + }, + "value": 0 + }, + { + "commandClassName": "Notification", + "commandClass": 113, + "endpoint": 0, + "property": "Power Management", + "propertyKey": "Over-load status", + "propertyName": "Power Management", + "propertyKeyName": "Over-load status", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Over-load status", + "states": { "0": "idle", "8": "Over-load detected" }, + "ccSpecific": { "notificationType": 8 } + }, + "value": 0 + } + ] + } \ No newline at end of file diff --git a/tests/fixtures/zwave_js/climate_danfoss_lc_13_state.json b/tests/fixtures/zwave_js/climate_danfoss_lc_13_state.json new file mode 100644 index 00000000000..e218d3b6a0e --- /dev/null +++ b/tests/fixtures/zwave_js/climate_danfoss_lc_13_state.json @@ -0,0 +1,368 @@ +{ + "nodeId": 5, + "index": 0, + "status": 1, + "ready": true, + "deviceClass": { + "basic": "Routing Slave", + "generic": "Thermostat", + "specific": "Setpoint Thermostat", + "mandatorySupportedCCs": [ + "Manufacturer Specific", + "Multi Command", + "Thermostat Setpoint", + "Version" + ], + "mandatoryControlCCs": [] + }, + "isListening": false, + "isFrequentListening": false, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": false, + "version": 4, + "isBeaming": true, + "manufacturerId": 2, + "productId": 4, + "productType": 5, + "firmwareVersion": "1.1", + "deviceConfig": { + "manufacturerId": 2, + "manufacturer": "Danfoss", + "label": "LC-13", + "description": "Living Connect Z Thermostat", + "devices": [ + { + "productType": "0x0005", + "productId": "0x0004" + }, + { + "productType": "0x8005", + "productId": "0x0001" + }, + { + "productType": "0x8005", + "productId": "0x0002" + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "associations": {}, + "compat": { + "valueIdRegex": {}, + "queryOnWakeup": [ + [ + "Battery", + "get" + ], + [ + "Thermostat Setpoint", + "get", + 1 + ] + ] + } + }, + "label": "LC-13", + "neighbors": [ + 1, + 14 + ], + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 5, + "index": 0 + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyName": "setpoint", + "propertyKeyName": "Heating", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "unit": "\u00b0C", + "ccSpecific": { + "setpointType": 1 + } + }, + "value": 25 + }, + { + "endpoint": 0, + "commandClass": 70, + "commandClassName": "Climate Control Schedule", + "property": "changeCounter", + "propertyName": "changeCounter", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + } + }, + { + "endpoint": 0, + "commandClass": 70, + "commandClassName": "Climate Control Schedule", + "property": "overrideType", + "propertyName": "overrideType", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 70, + "commandClassName": "Climate Control Schedule", + "property": "overrideState", + "propertyName": "overrideState", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "Unused" + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 4 + }, + { + "endpoint": 0, + "commandClass": 117, + "commandClassName": "Protection", + "property": "local", + "propertyName": "local", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Local protection state", + "states": { + "0": "Unprotected", + "2": "NoOperationPossible" + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 117, + "commandClassName": "Protection", + "property": "rf", + "propertyName": "rf", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "RF protection state", + "states": {} + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 117, + "commandClassName": "Protection", + "property": "exclusiveControlNodeId", + "propertyName": "exclusiveControlNodeId", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + } + }, + { + "endpoint": 0, + "commandClass": 117, + "commandClassName": "Protection", + "property": "timeout", + "propertyName": "timeout", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + } + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "level", + "propertyName": "level", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 100, + "unit": "%", + "label": "Battery level" + }, + "value": 53 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "isLow", + "propertyName": "isLow", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 132, + "commandClassName": "Wake Up", + "property": "wakeUpInterval", + "propertyName": "wakeUpInterval", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "min": 60, + "max": 1800, + "label": "Wake Up interval", + "steps": 60, + "default": 300 + }, + "value": 300 + }, + { + "endpoint": 0, + "commandClass": 132, + "commandClassName": "Wake Up", + "property": "controllerNodeId", + "propertyName": "controllerNodeId", + "ccVersion": 1, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Node ID of the controller" + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 1, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 6 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 1, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "3.67" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 1, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": [ + "1.1" + ] + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/zwave_js/climate_heatit_z_trm3_state.json b/tests/fixtures/zwave_js/climate_heatit_z_trm3_state.json new file mode 100644 index 00000000000..066811c7374 --- /dev/null +++ b/tests/fixtures/zwave_js/climate_heatit_z_trm3_state.json @@ -0,0 +1,1181 @@ +{ + "nodeId": 24, + "index": 0, + "installerIcon": 4608, + "userIcon": 4609, + "status": 4, + "ready": true, + "deviceClass": { + "basic": "Routing Slave", + "generic": "Thermostat", + "specific": "Thermostat General V2", + "mandatorySupportedCCs": [ + "Basic", + "Manufacturer Specific", + "Thermostat Mode", + "Thermostat Setpoint", + "Version" + ], + "mandatoryControlCCs": [] + }, + "isListening": true, + "isFrequentListening": false, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": false, + "version": 4, + "isBeaming": true, + "manufacturerId": 411, + "productId": 515, + "productType": 3, + "firmwareVersion": "4.0", + "zwavePlusVersion": 1, + "nodeType": 0, + "roleType": 5, + "deviceConfig": { + "manufacturerId": 411, + "manufacturer": "ThermoFloor", + "label": "Heatit Z-TRM3", + "description": "Floor thermostat", + "devices": [ + { + "productType": "0x0003", + "productId": "0x0203" + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + }, + "compat": { + "valueIdRegex": {}, + "overrideFloatEncoding": { + "size": 2 + }, + "addCCs": {} + } + }, + "label": "Heatit Z-TRM3", + "neighbors": [ + 1, + 2, + 3, + 4, + 6, + 9, + 10, + 11, + 12, + 13, + 14, + 15, + 17, + 18, + 19, + 25, + 26, + 28 + ], + "endpointCountIsDynamic": false, + "endpointsHaveIdenticalCapabilities": false, + "individualEndpointCount": 4, + "aggregatedEndpointCount": 0, + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 24, + "index": 0, + "installerIcon": 4608, + "userIcon": 4609 + }, + { + "nodeId": 24, + "index": 1, + "installerIcon": 4608, + "userIcon": 4609 + }, + { + "nodeId": 24, + "index": 2, + "installerIcon": 3328, + "userIcon": 3329 + }, + { + "nodeId": 24, + "index": 3, + "installerIcon": 3328, + "userIcon": 3329 + }, + { + "nodeId": 24, + "index": 4, + "installerIcon": 3328, + "userIcon": 3329 + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyName": "param001", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyName": "Sensor mode", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 4, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "F-mode, floor sensor mode", + "1": "A-mode, internal room sensor mode", + "2": "AF-mode, internal sensor and floor sensor mode", + "3": "A2-mode, external room sensor mode", + "4": "A2F-mode, external sensor with floor limitation" + }, + "label": "Sensor mode", + "description": "Sensor mode", + "isFromConfig": true + }, + "value": 2 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "Floor sensor type", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 5, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "10K-NTC", + "1": "12K-NTC", + "2": "15K-NTC", + "3": "22K-NTC", + "4": "33K-NTC", + "5": "47K-NTC" + }, + "label": "Floor sensor type", + "description": "Floor sensor type", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "Temperature control hysteresis (DIFF I)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 3, + "max": 30, + "default": 5, + "format": 0, + "allowManualEntry": true, + "label": "Temperature control hysteresis (DIFF I)", + "description": "Temperature control hysteresis (DIFF I), 1 equals 0.1 \u00b0C", + "isFromConfig": true + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 5, + "propertyName": "Floor minimum temperature limit (FLo)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 50, + "max": 400, + "default": 50, + "format": 0, + "allowManualEntry": true, + "label": "Floor minimum temperature limit (FLo)", + "description": "Floor minimum temperature limit (FLo), 1 equals 0.1 \u00b0C", + "isFromConfig": true + }, + "value": 50 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 6, + "propertyName": "Floor maximum temperature (FHi)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 50, + "max": 400, + "default": 400, + "format": 0, + "allowManualEntry": true, + "label": "Floor maximum temperature (FHi)", + "description": "Floor maximum temperature (FHi), 1 equals 0.1 \u00b0C", + "isFromConfig": true + }, + "value": 400 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 7, + "propertyName": "Air minimum temperature limit (ALo)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 50, + "max": 400, + "default": 50, + "format": 0, + "allowManualEntry": true, + "label": "Air minimum temperature limit (ALo)", + "description": "Air minimum temperature limit (ALo), 1 equals 0.1 \u00b0C", + "isFromConfig": true + }, + "value": 50 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 8, + "propertyName": "Air maximum temperature limit (AHi)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 50, + "max": 400, + "default": 400, + "format": 0, + "allowManualEntry": true, + "label": "Air maximum temperature limit (AHi)", + "description": "Air maximum temperature limit (AHi), 1 equals 0.1 \u00b0C", + "isFromConfig": true + }, + "value": 400 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 9, + "propertyName": "param009", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 225 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 10, + "propertyName": "Room sensor calibration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": -60, + "max": 60, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Room sensor calibration", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 11, + "propertyName": "Floor sensor calibration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": -60, + "max": 60, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Floor sensor calibration", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 12, + "propertyName": "External sensor calibration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": -60, + "max": 60, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "External sensor calibration", + "isFromConfig": true + }, + "value": -42 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 13, + "propertyName": "Temperature display", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Display setpoint temperature", + "1": "Display calculated temperature" + }, + "label": "Temperature display", + "description": "Selects which temperature is shown on the display.", + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 14, + "propertyName": "Button brightness - dimmed state", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 50, + "format": 0, + "allowManualEntry": true, + "label": "Button brightness - dimmed state", + "description": "Button brightness - dimmed state", + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 15, + "propertyName": "Button brightness - active state", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 100, + "format": 0, + "allowManualEntry": true, + "label": "Button brightness - active state", + "description": "Button brightness - active state", + "isFromConfig": true + }, + "value": 50 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 16, + "propertyName": "Display brightness - dimmed state", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 50, + "format": 0, + "allowManualEntry": true, + "label": "Display brightness - dimmed state", + "description": "Display brightness - dimmed state", + "isFromConfig": true + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 17, + "propertyName": "Display brightness - active state", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 100, + "format": 0, + "allowManualEntry": true, + "label": "Display brightness - active state", + "description": "Display brightness - active state", + "isFromConfig": true + }, + "value": 50 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 18, + "propertyName": "Temperature report interval", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 0, + "max": 32767, + "default": 60, + "format": 0, + "allowManualEntry": true, + "label": "Temperature report interval", + "description": "Temperature report interval", + "isFromConfig": true + }, + "value": 360 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 19, + "propertyName": "Temperature report hysteresis", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 100, + "default": 10, + "format": 0, + "allowManualEntry": true, + "label": "Temperature report hysteresis", + "description": "Temperature report hysteresis", + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 20, + "propertyName": "Meter report interval", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 0, + "max": 32767, + "default": 90, + "format": 0, + "allowManualEntry": true, + "label": "Meter report interval", + "description": "Meter report interval", + "isFromConfig": true + }, + "value": 3600 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 21, + "propertyName": "Meter report delta value", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 10, + "format": 1, + "allowManualEntry": true, + "label": "Meter report delta value", + "description": "Meter report delta value", + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 411 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 515 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "6.7" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": [ + "4.0" + ] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "sdkVersion", + "propertyName": "sdkVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "6.81.6" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkAPIVersion", + "propertyName": "applicationFrameworkAPIVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "4.3.0" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkBuildNumber", + "propertyName": "applicationFrameworkBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 52445 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceVersion", + "propertyName": "hostInterfaceVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "unused" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceBuildNumber", + "propertyName": "hostInterfaceBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolVersion", + "propertyName": "zWaveProtocolVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "6.7.0" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolBuildNumber", + "propertyName": "zWaveProtocolBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 97 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationVersion", + "propertyName": "applicationVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "4.0.33" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationBuildNumber", + "propertyName": "applicationBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 52445 + }, + { + "endpoint": 1, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyName": "setpoint", + "propertyKeyName": "Heating", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 5, + "max": 35, + "unit": "\u00b0C", + "ccSpecific": { + "setpointType": 1 + } + }, + "value": 22.5 + }, + { + "endpoint": 1, + "commandClass": 64, + "commandClassName": "Thermostat Mode", + "property": "mode", + "propertyName": "mode", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 31, + "label": "Thermostat mode", + "states": { + "0": "Off", + "1": "Heat" + } + }, + "value": 1 + }, + { + "endpoint": 1, + "commandClass": 64, + "commandClassName": "Thermostat Mode", + "property": "manufacturerData", + "propertyName": "manufacturerData", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + } + }, + { + "endpoint": 1, + "commandClass": 66, + "commandClassName": "Thermostat Operating State", + "property": "state", + "propertyName": "state", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Operating state", + "states": { + "0": "Idle", + "1": "Heating", + "2": "Cooling", + "3": "Fan Only", + "4": "Pending Heat", + "5": "Pending Cool", + "6": "Vent/Economizer", + "7": "Aux Heating", + "8": "2nd Stage Heating", + "9": "2nd Stage Cooling", + "10": "2nd Stage Aux Heat", + "11": "3rd Stage Aux Heat" + } + }, + "value": 0 + }, + { + "endpoint": 1, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyName": "value", + "propertyKeyName": "Electric_kWh_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Value (Electric, Consumed)", + "unit": "kWh", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 0 + } + }, + "value": 369.2 + }, + { + "endpoint": 1, + "commandClass": 50, + "commandClassName": "Meter", + "property": "deltaTime", + "propertyName": "deltaTime", + "propertyKeyName": "Electric_kWh_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Time since the previous reading", + "unit": "s", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 0 + } + }, + "value": 0 + }, + { + "endpoint": 1, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyName": "value", + "propertyKeyName": "Electric_W_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Value (Electric, Consumed)", + "unit": "W", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 2 + } + }, + "value": 0.09 + }, + { + "endpoint": 1, + "commandClass": 50, + "commandClassName": "Meter", + "property": "deltaTime", + "propertyName": "deltaTime", + "propertyKeyName": "Electric_W_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Time since the previous reading", + "unit": "s", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 2 + } + }, + "value": 0 + }, + { + "endpoint": 1, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyName": "value", + "propertyKeyName": "Electric_V_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Value (Electric, Consumed)", + "unit": "V", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 4 + } + }, + "value": 238 + }, + { + "endpoint": 1, + "commandClass": 50, + "commandClassName": "Meter", + "property": "deltaTime", + "propertyName": "deltaTime", + "propertyKeyName": "Electric_V_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Time since the previous reading", + "unit": "s", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 4 + } + }, + "value": 0 + }, + { + "endpoint": 1, + "commandClass": 50, + "commandClassName": "Meter", + "property": "previousValue", + "propertyName": "previousValue", + "propertyKeyName": "Electric_kWh_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Previous value (Electric, Consumed)", + "unit": "kWh", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 0 + } + } + }, + { + "endpoint": 1, + "commandClass": 50, + "commandClassName": "Meter", + "property": "previousValue", + "propertyName": "previousValue", + "propertyKeyName": "Electric_W_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Previous value (Electric, Consumed)", + "unit": "W", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 2 + } + } + }, + { + "endpoint": 1, + "commandClass": 50, + "commandClassName": "Meter", + "property": "previousValue", + "propertyName": "previousValue", + "propertyKeyName": "Electric_V_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Previous value (Electric, Consumed)", + "unit": "V", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 4 + } + } + }, + { + "endpoint": 2, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "unit": "\u00b0C", + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 0 + } + }, + "value": 22.9 + }, + { + "endpoint": 3, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "unit": "\u00b0C", + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 0 + } + }, + "value": 0 + }, + { + "endpoint": 4, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "unit": "\u00b0C", + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 0 + } + }, + "value": 25.5 + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_different_endpoints_state.json b/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_different_endpoints_state.json new file mode 100644 index 00000000000..ea38dfd9d6b --- /dev/null +++ b/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_different_endpoints_state.json @@ -0,0 +1,727 @@ +{ + "nodeId": 26, + "index": 0, + "installerIcon": 4608, + "userIcon": 4608, + "status": 4, + "ready": true, + "deviceClass": { + "basic": "Routing Slave", + "generic": "Thermostat", + "specific": "Thermostat General V2", + "mandatorySupportedCCs": [ + "Basic", + "Manufacturer Specific", + "Thermostat Mode", + "Thermostat Setpoint", + "Version" + ], + "mandatoryControlCCs": [] + }, + "isListening": true, + "isFrequentListening": false, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": false, + "version": 4, + "isBeaming": true, + "manufacturerId": 152, + "productId": 256, + "productType": 25602, + "firmwareVersion": "10.7", + "zwavePlusVersion": 1, + "nodeType": 0, + "roleType": 5, + "deviceConfig": { + "manufacturerId": 152, + "manufacturer": "Radio Thermostat Company of America (RTC)", + "label": "CT100 Plus", + "description": "Z-Wave Thermostat", + "devices": [{ "productType": "0x6402", "productId": "0x0100" }], + "firmwareVersion": { "min": "0.0", "max": "255.255" }, + "paramInformation": { "_map": {} } + }, + "label": "CT100 Plus", + "neighbors": [1, 2, 3, 4, 23], + "endpointCountIsDynamic": false, + "endpointsHaveIdenticalCapabilities": false, + "individualEndpointCount": 2, + "aggregatedEndpointCount": 0, + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 26, + "index": 0, + "installerIcon": 4608, + "userIcon": 4608 + }, + { "nodeId": 26, "index": 1 }, + { + "nodeId": 26, + "index": 2, + "installerIcon": 3328, + "userIcon": 3333 + } + ], + "values": [ + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "manufacturerId", + "propertyName": "manufacturerId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 152, + "ccVersion": 2 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productType", + "propertyName": "productType", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 25602, + "ccVersion": 2 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productId", + "propertyName": "productId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 256, + "ccVersion": 2 + }, + { + "commandClassName": "Thermostat Mode", + "commandClass": 64, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 31, + "label": "Thermostat mode", + "states": { + "0": "Off", + "1": "Heat", + "2": "Cool", + "3": "Auto", + "11": "Energy heat", + "12": "Energy cool" + } + }, + "value": 1, + "ccVersion": 2 + }, + { + "commandClassName": "Thermostat Mode", + "commandClass": 64, + "endpoint": 0, + "property": "manufacturerData", + "propertyName": "manufacturerData", + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "ccVersion": 2 + }, + { + "commandClassName": "Thermostat Setpoint", + "commandClass": 67, + "endpoint": 0, + "property": "setpoint", + "propertyKey": 1, + "propertyName": "setpoint", + "propertyKeyName": "Heating", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "unit": "\u00b0F", + "ccSpecific": { "setpointType": 1 } + }, + "value": 72, + "ccVersion": 2 + }, + { + "commandClassName": "Thermostat Setpoint", + "commandClass": 67, + "endpoint": 0, + "property": "setpoint", + "propertyKey": 2, + "propertyName": "setpoint", + "propertyKeyName": "Cooling", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "unit": "\u00b0F", + "ccSpecific": { "setpointType": 2 } + }, + "value": 73, + "ccVersion": 2 + }, + { + "commandClassName": "Thermostat Setpoint", + "commandClass": 67, + "endpoint": 0, + "property": "setpoint", + "propertyKey": 11, + "propertyName": "setpoint", + "propertyKeyName": "Energy Save Heating", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "unit": "\u00b0F", + "ccSpecific": { "setpointType": 11 } + }, + "value": 62, + "ccVersion": 2 + }, + { + "commandClassName": "Thermostat Setpoint", + "commandClass": 67, + "endpoint": 0, + "property": "setpoint", + "propertyKey": 12, + "propertyName": "setpoint", + "propertyKeyName": "Energy Save Cooling", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "unit": "\u00b0F", + "ccSpecific": { "setpointType": 12 } + }, + "value": 85, + "ccVersion": 2 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "libraryType", + "propertyName": "libraryType", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3, + "ccVersion": 2 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "protocolVersion", + "propertyName": "protocolVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.24", + "ccVersion": 2 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["10.7"], + "ccVersion": 2 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + }, + "ccVersion": 2 + }, + { + "commandClassName": "Indicator", + "commandClass": 135, + "endpoint": 0, + "property": "value", + "propertyName": "value", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 255, + "label": "Indicator value", + "ccSpecific": { "indicatorId": 0 } + }, + "value": 0, + "ccVersion": 1 + }, + { + "commandClassName": "Thermostat Operating State", + "commandClass": 66, + "endpoint": 0, + "property": "state", + "propertyName": "state", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Operating state", + "states": { + "0": "Idle", + "1": "Heating", + "2": "Cooling", + "3": "Fan Only", + "4": "Pending Heat", + "5": "Pending Cool", + "6": "Vent/Economizer", + "7": "Aux Heating", + "8": "2nd Stage Heating", + "9": "2nd Stage Cooling", + "10": "2nd Stage Aux Heat", + "11": "3rd Stage Aux Heat" + } + }, + "value": 0, + "ccVersion": 2 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 1, + "propertyName": "Temperature Reporting Threshold", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 4, + "default": 2, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Disabled", + "1": "0.5\u00b0 F", + "2": "1.0\u00b0 F", + "3": "1.5\u00b0 F", + "4": "2.0\u00b0 F" + }, + "label": "Temperature Reporting Threshold", + "description": "Reporting threshold for changes in the ambient temperature", + "isFromConfig": true + }, + "value": 2, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 2, + "propertyName": "HVAC Settings", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "valueSize": 4, + "min": 0, + "max": 0, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "HVAC Settings", + "description": "Configured HVAC settings", + "isFromConfig": true + }, + "value": 17891329, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 4, + "propertyName": "Power Status", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "valueSize": 1, + "min": 0, + "max": 0, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Power Status", + "description": "C-Wire / Battery Status", + "isFromConfig": true + }, + "value": 1, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 5, + "propertyName": "Humidity Reporting Threshold", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": false, + "states": { + "0": "Disabled", + "1": "3% RH", + "2": "5% RH", + "3": "10% RH" + }, + "label": "Humidity Reporting Threshold", + "description": "Reporting threshold for changes in the relative humidity", + "isFromConfig": true + }, + "value": 2, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 6, + "propertyName": "Auxiliary/Emergency", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": false, + "states": { + "0": "Auxiliary/Emergency heat disabled", + "1": "Auxiliary/Emergency heat enabled" + }, + "label": "Auxiliary/Emergency", + "description": "Enables or disables auxiliary / emergency heating", + "isFromConfig": true + }, + "value": 0, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 7, + "propertyName": "Thermostat Swing Temperature", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 8, + "default": 2, + "format": 0, + "allowManualEntry": false, + "states": { + "1": "0.5\u00b0 F", + "2": "1.0\u00b0 F", + "3": "1.5\u00b0 F", + "4": "2.0\u00b0 F", + "5": "2.5\u00b0 F", + "6": "3.0\u00b0 F", + "7": "3.5\u00b0 F", + "8": "4.0\u00b0 F" + }, + "label": "Thermostat Swing Temperature", + "description": "Variance allowed from setpoint to engage HVAC", + "isFromConfig": true + }, + "value": 2, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 8, + "propertyName": "Thermostat Diff Temperature", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 4, + "max": 12, + "default": 4, + "format": 0, + "allowManualEntry": false, + "states": { + "4": "2.0\u00b0 F", + "8": "4.0\u00b0 F", + "12": "6.0\u00b0 F" + }, + "label": "Thermostat Diff Temperature", + "description": "Configures additional stages", + "isFromConfig": true + }, + "value": 1028, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 9, + "propertyName": "Thermostat Recovery Mode", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 2, + "default": 2, + "format": 0, + "allowManualEntry": false, + "states": { + "1": "Fast recovery mode", + "2": "Economy recovery mode" + }, + "label": "Thermostat Recovery Mode", + "description": "Fast or Economy recovery mode", + "isFromConfig": true + }, + "value": 2, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 10, + "propertyName": "Temperature Reporting Filter", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 124, + "default": 124, + "format": 0, + "allowManualEntry": true, + "label": "Temperature Reporting Filter", + "description": "Upper/Lower bounds for thermostat temperature reporting", + "isFromConfig": true + }, + "value": 32000, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 11, + "propertyName": "Simple UI Mode", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Normal mode enabled", + "1": "Simple mode enabled" + }, + "label": "Simple UI Mode", + "description": "Simple mode enable/disable", + "isFromConfig": true + }, + "value": 1, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 12, + "propertyName": "Multicast", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Multicast disabled", + "1": "Multicast enabled" + }, + "label": "Multicast", + "description": "Enable or disables Multicast", + "isFromConfig": true + }, + "value": 0, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 3, + "propertyName": "Utility Lock Enable/Disable", + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": false, + "states": { + "0": "Utility lock disabled", + "1": "Utility lock enabled" + }, + "label": "Utility Lock Enable/Disable", + "description": "Prevents setpoint changes at thermostat", + "isFromConfig": true + }, + "ccVersion": 1 + }, + { + "commandClassName": "Battery", + "commandClass": 128, + "endpoint": 0, + "property": "level", + "propertyName": "level", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 100, + "unit": "%", + "label": "Battery level" + }, + "value": 100, + "ccVersion": 1 + }, + { + "commandClassName": "Battery", + "commandClass": 128, + "endpoint": 0, + "property": "isLow", + "propertyName": "isLow", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false, + "ccVersion": 1 + }, + { + "commandClassName": "Multilevel Sensor", + "commandClass": 49, + "endpoint": 2, + "property": "Air temperature", + "propertyName": "Air temperature", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "unit": "\u00b0F", + "label": "Air temperature", + "ccSpecific": { "sensorType": 1, "scale": 1 } + }, + "value": 72.5, + "ccVersion": 5 + }, + { + "commandClassName": "Multilevel Sensor", + "commandClass": 49, + "endpoint": 2, + "property": "Humidity", + "propertyName": "Humidity", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "unit": "%", + "label": "Humidity", + "ccSpecific": { "sensorType": 5, "scale": 0 } + }, + "value": 20, + "ccVersion": 5 + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_state.json b/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_state.json new file mode 100644 index 00000000000..77a68aafde1 --- /dev/null +++ b/tests/fixtures/zwave_js/climate_radio_thermostat_ct100_plus_state.json @@ -0,0 +1,693 @@ +{ + "nodeId": 13, + "index": 0, + "installerIcon": 4608, + "userIcon": 4608, + "status": 4, + "ready": true, + "deviceClass": { + "basic": "Static Controller", + "generic": "Thermostat", + "specific": "Thermostat General V2", + "mandatorySupportedCCs": [ + "Basic", + "Manufacturer Specific", + "Thermostat Mode", + "Thermostat Setpoint", + "Version" + ], + "mandatoryControlCCs": [] + }, + "isListening": true, + "isFrequentListening": false, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": false, + "version": 4, + "isBeaming": true, + "manufacturerId": 152, + "productId": 256, + "productType": 25602, + "firmwareVersion": "10.7", + "zwavePlusVersion": 1, + "nodeType": 0, + "roleType": 5, + "deviceConfig": { + "manufacturerId": 152, + "manufacturer": "Radio Thermostat Company of America (RTC)", + "label": "CT100 Plus", + "description": "Z-Wave Thermostat", + "devices": [{ "productType": "0x6402", "productId": "0x0100" }], + "firmwareVersion": { "min": "0.0", "max": "255.255" }, + "paramInformation": { "_map": {} } + }, + "label": "CT100 Plus", + "neighbors": [1, 2, 3, 4, 20], + "endpointCountIsDynamic": false, + "endpointsHaveIdenticalCapabilities": false, + "individualEndpointCount": 2, + "aggregatedEndpointCount": 0, + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 13, + "index": 0, + "installerIcon": 4608, + "userIcon": 4608 + }, + { + "nodeId": 13, + "index": 1, + "installerIcon": 4608, + "userIcon": 4608 + }, + { "nodeId": 13, "index": 2 } + ], + "values": [ + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "manufacturerId", + "propertyName": "manufacturerId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 152 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productType", + "propertyName": "productType", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 25602 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productId", + "propertyName": "productId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 256 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "libraryType", + "propertyName": "libraryType", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "protocolVersion", + "propertyName": "protocolVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.24" + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["10.7"] + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + }, + { + "commandClassName": "Indicator", + "commandClass": 135, + "endpoint": 0, + "property": "value", + "propertyName": "value", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 255, + "label": "Indicator value", + "ccSpecific": { "indicatorId": 0 } + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 1, + "propertyName": "Temperature Reporting Threshold", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 4, + "default": 2, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Disabled", + "1": "0.5° F", + "2": "1.0° F", + "3": "1.5° F", + "4": "2.0° F" + }, + "label": "Temperature Reporting Threshold", + "description": "Reporting threshold for changes in the ambient temperature", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 2, + "propertyName": "HVAC Settings", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "valueSize": 4, + "min": 0, + "max": 0, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "HVAC Settings", + "description": "Configured HVAC settings", + "isFromConfig": true + }, + "value": 17891329 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 4, + "propertyName": "Power Status", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "valueSize": 1, + "min": 0, + "max": 0, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Power Status", + "description": "C-Wire / Battery Status", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 7, + "propertyName": "Thermostat Swing Temperature", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 8, + "default": 2, + "format": 0, + "allowManualEntry": false, + "states": { + "1": "0.5° F", + "2": "1.0° F", + "3": "1.5° F", + "4": "2.0° F", + "5": "2.5° F", + "6": "3.0° F", + "7": "3.5° F", + "8": "4.0° F" + }, + "label": "Thermostat Swing Temperature", + "description": "Variance allowed from setpoint to engage HVAC", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 8, + "propertyName": "Thermostat Diff Temperature", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 4, + "max": 12, + "default": 4, + "format": 0, + "allowManualEntry": false, + "states": { "4": "2.0° F", "8": "4.0° F", "12": "6.0° F" }, + "label": "Thermostat Diff Temperature", + "description": "Configures additional stages", + "isFromConfig": true + }, + "value": 1028 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 9, + "propertyName": "Thermostat Recovery Mode", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 2, + "default": 2, + "format": 0, + "allowManualEntry": false, + "states": { + "1": "Fast recovery mode", + "2": "Economy recovery mode" + }, + "label": "Thermostat Recovery Mode", + "description": "Fast or Economy recovery mode", + "isFromConfig": true + }, + "value": 2 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 10, + "propertyName": "Temperature Reporting Filter", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 124, + "default": 124, + "format": 0, + "allowManualEntry": true, + "label": "Temperature Reporting Filter", + "description": "Upper/Lower bounds for thermostat temperature reporting", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 11, + "propertyName": "Simple UI Mode", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Normal mode enabled", + "1": "Simple mode enabled" + }, + "label": "Simple UI Mode", + "description": "Simple mode enable/disable", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 12, + "propertyName": "Multicast", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Multicast disabled", + "1": "Multicast enabled" + }, + "label": "Multicast", + "description": "Enable or disables Multicast", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 3, + "propertyName": "Utility Lock Enable/Disable", + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": false, + "states": { + "0": "Utility lock disabled", + "1": "Utility lock enabled" + }, + "label": "Utility Lock Enable/Disable", + "description": "Prevents setpoint changes at thermostat", + "isFromConfig": true + } + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 5, + "propertyName": "Humidity Reporting Threshold", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": false, + "states": { + "0": "Disabled", + "1": "3% RH", + "2": "5% RH", + "3": "10% RH" + }, + "label": "Humidity Reporting Threshold", + "description": "Reporting threshold for changes in the relative humidity", + "isFromConfig": true + } + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 6, + "propertyName": "Auxiliary/Emergency", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": false, + "states": { + "0": "Auxiliary/Emergency heat disabled", + "1": "Auxiliary/Emergency heat enabled" + }, + "label": "Auxiliary/Emergency", + "description": "Enables or disables auxiliary / emergency heating", + "isFromConfig": true + } + }, + { + "commandClassName": "Battery", + "commandClass": 128, + "endpoint": 0, + "property": "level", + "propertyName": "level", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 100, + "unit": "%", + "label": "Battery level" + }, + "value": 100 + }, + { + "commandClassName": "Battery", + "commandClass": 128, + "endpoint": 0, + "property": "isLow", + "propertyName": "isLow", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false + }, + { + "commandClassName": "Indicator", + "commandClass": 135, + "endpoint": 1, + "property": "value", + "propertyName": "value", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 255, + "label": "Indicator value", + "ccSpecific": { "indicatorId": 0 } + }, + "value": 0 + }, + { + "commandClassName": "Multilevel Sensor", + "commandClass": 49, + "endpoint": 1, + "property": "Air temperature", + "propertyName": "Air temperature", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "unit": "°F", + "label": "Air temperature", + "ccSpecific": { "sensorType": 1, "scale": 1 } + }, + "value": 72 + }, + { + "commandClassName": "Multilevel Sensor", + "commandClass": 49, + "endpoint": 1, + "property": "Humidity", + "propertyName": "Humidity", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "unit": "%", + "label": "Humidity", + "ccSpecific": { "sensorType": 5, "scale": 0 } + }, + "value": 30 + }, + { + "commandClassName": "Thermostat Mode", + "commandClass": 64, + "endpoint": 1, + "property": "mode", + "propertyName": "mode", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 31, + "label": "Thermostat mode", + "states": { "0": "Off", "1": "Heat", "2": "Cool", "3": "Auto" } + }, + "value": 1 + }, + { + "commandClassName": "Thermostat Mode", + "commandClass": 64, + "endpoint": 1, + "property": "manufacturerData", + "propertyName": "manufacturerData", + "metadata": { "type": "any", "readable": true, "writeable": true } + }, + { + "commandClassName": "Thermostat Operating State", + "commandClass": 66, + "endpoint": 1, + "property": "state", + "propertyName": "state", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Operating state", + "states": { + "0": "Idle", + "1": "Heating", + "2": "Cooling", + "3": "Fan Only", + "4": "Pending Heat", + "5": "Pending Cool", + "6": "Vent/Economizer", + "7": "Aux Heating", + "8": "2nd Stage Heating", + "9": "2nd Stage Cooling", + "10": "2nd Stage Aux Heat", + "11": "3rd Stage Aux Heat" + } + }, + "value": 0 + }, + { + "commandClassName": "Thermostat Setpoint", + "commandClass": 67, + "endpoint": 1, + "property": "setpoint", + "propertyKey": 1, + "propertyName": "setpoint", + "propertyKeyName": "Heating", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "unit": "°F", + "ccSpecific": { "setpointType": 1 } + }, + "value": 72 + }, + { + "commandClassName": "Thermostat Setpoint", + "commandClass": 67, + "endpoint": 1, + "property": "setpoint", + "propertyKey": 2, + "propertyName": "setpoint", + "propertyKeyName": "Cooling", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "unit": "°F", + "ccSpecific": { "setpointType": 2 } + }, + "value": 73 + }, + { + "commandClassName": "Battery", + "commandClass": 128, + "endpoint": 1, + "property": "level", + "propertyName": "level", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 100, + "unit": "%", + "label": "Battery level" + }, + "value": 100 + }, + { + "commandClassName": "Battery", + "commandClass": 128, + "endpoint": 1, + "property": "isLow", + "propertyName": "isLow", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false + } + ] +} diff --git a/tests/fixtures/zwave_js/controller_state.json b/tests/fixtures/zwave_js/controller_state.json new file mode 100644 index 00000000000..df026e8fd2c --- /dev/null +++ b/tests/fixtures/zwave_js/controller_state.json @@ -0,0 +1,98 @@ +{ + "controller": { + "libraryVersion": "Z-Wave 3.95", + "type": 1, + "homeId": 3245146787, + "ownNodeId": 1, + "isSecondary": false, + "isUsingHomeIdFromOtherNetwork": false, + "isSISPresent": true, + "wasRealPrimary": true, + "isStaticUpdateController": true, + "isSlave": false, + "serialApiVersion": "1.0", + "manufacturerId": 134, + "productType": 257, + "productId": 90, + "supportedFunctionTypes": [ + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 16, + 17, + 18, + 19, + 20, + 21, + 22, + 23, + 24, + 28, + 32, + 33, + 34, + 35, + 36, + 39, + 41, + 42, + 43, + 44, + 45, + 65, + 66, + 68, + 69, + 70, + 71, + 72, + 73, + 74, + 75, + 76, + 77, + 80, + 81, + 83, + 84, + 85, + 86, + 87, + 94, + 96, + 97, + 98, + 99, + 102, + 103, + 128, + 144, + 146, + 147, + 152, + 180, + 182, + 183, + 184, + 185, + 186, + 189, + 190, + 191, + 210, + 211, + 212, + 238, + 239 + ], + "sucNodeId": 1, + "supportsTimers": false + }, + "nodes": [ + ] +} diff --git a/tests/fixtures/zwave_js/eaton_rf9640_dimmer_state.json b/tests/fixtures/zwave_js/eaton_rf9640_dimmer_state.json new file mode 100644 index 00000000000..0f2f45d01e3 --- /dev/null +++ b/tests/fixtures/zwave_js/eaton_rf9640_dimmer_state.json @@ -0,0 +1,782 @@ +{ + "nodeId": 19, + "index": 0, + "installerIcon": 1536, + "userIcon": 1536, + "status": 4, + "ready": true, + "deviceClass": { + "basic": "Routing Slave", + "generic": "Multilevel Switch", + "specific": "Multilevel Power Switch", + "mandatorySupportedCCs": [ + "Basic", + "Multilevel Switch", + "All Switch" + ], + "mandatoryControlCCs": [] + }, + "isListening": true, + "isFrequentListening": false, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": false, + "version": 4, + "isBeaming": true, + "manufacturerId": 26, + "productId": 1281, + "productType": 17481, + "firmwareVersion": "1.0", + "zwavePlusVersion": 1, + "nodeType": 0, + "roleType": 5, + "name": "AllLoadDimmer", + "location": "", + "deviceConfig": { + "manufacturerId": 26, + "manufacturer": "Eaton", + "label": "RF9640", + "description": "Z-Wave Plus universal smart dimmer", + "devices": [ + { + "productType": "0x4449", + "productId": "0x0501" + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + } + }, + "label": "RF9640", + "neighbors": [ + 4, + 8, + 9, + 11, + 12, + 13, + 14, + 15, + 16, + 17, + 18 + ], + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 19, + "index": 0, + "installerIcon": 1536, + "userIcon": 1536 + } + ], + "values": [ + { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 99, + "label": "Target value" + }, + "value": 20, + "ccVersion": 4 + }, + { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "duration", + "propertyName": "duration", + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Transition duration" + }, + "value": { + "value": 0, + "unit": "seconds" + }, + "ccVersion": 4 + }, + { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "currentValue", + "propertyName": "currentValue", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 99, + "label": "Current value" + }, + "value": 22, + "ccVersion": 4 + }, + { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "Up", + "propertyName": "Up", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { + "switchType": 2 + } + }, + "ccVersion": 4 + }, + { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "Down", + "propertyName": "Down", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { + "switchType": 2 + } + }, + "ccVersion": 4 + }, + { + "commandClassName": "Scene Activation", + "commandClass": 43, + "endpoint": 0, + "property": "sceneId", + "propertyName": "sceneId", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 1, + "max": 255, + "label": "Scene ID" + }, + "ccVersion": 0 + }, + { + "commandClassName": "Scene Activation", + "commandClass": 43, + "endpoint": 0, + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Dimming duration" + }, + "ccVersion": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 1, + "propertyName": "Delayed OFF time", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": true, + "label": "Delayed OFF time", + "description": "Time in seconds to delay OFF", + "isFromConfig": true + }, + "value": 10, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 2, + "propertyName": "Panic ON time", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": true, + "label": "Panic ON time", + "description": "Time in seconds for panic mode ON", + "isFromConfig": true + }, + "value": 1, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 3, + "propertyName": "Panic OFF time", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": true, + "label": "Panic OFF time", + "description": "time in seconds for OFF in panic mode", + "isFromConfig": true + }, + "value": 1, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 5, + "propertyName": "Power Up State", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 3, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "1": "OFF", + "2": "ON", + "3": "Last State" + }, + "label": "Power Up State", + "description": "Power Up State of the switch", + "isFromConfig": true + }, + "value": 1, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 6, + "propertyName": "Panic mode enable", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "OFF", + "1": "ON" + }, + "label": "Panic mode enable", + "description": "Enables this switch to participate in panic mode", + "isFromConfig": true + }, + "value": 0, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 7, + "propertyName": "Dimmer Ramp Time", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 1, + "format": 1, + "allowManualEntry": true, + "label": "Dimmer Ramp Time", + "description": "Time in seconds to reach desired level", + "isFromConfig": true + }, + "value": 3, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 8, + "propertyName": "Kickstart / Rapid Start", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "disables", + "1": "enables" + }, + "label": "Kickstart / Rapid Start", + "description": "Ensures that LED / CFL bulbs turn on when the preset dim level is low", + "isFromConfig": true + }, + "value": 0, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 9, + "propertyName": "Reset max/min levels to factory default", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 0, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Reset max/min levels to factory default", + "description": "Reset max/min levels to factory default", + "isFromConfig": true + }, + "value": 1, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 12, + "propertyName": "Maximum Dimming Level", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 99, + "default": 99, + "format": 0, + "allowManualEntry": true, + "label": "Maximum Dimming Level", + "isFromConfig": true + }, + "value": 99, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 13, + "propertyName": "Blue LED brightness level while dimmer is ON", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 4, + "default": 4, + "format": 0, + "allowManualEntry": true, + "label": "Blue LED brightness level while dimmer is ON", + "isFromConfig": true + }, + "value": 3, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 14, + "propertyName": "Blue LED brightness level while dimmer is OFF", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 4, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Blue LED brightness level while dimmer is OFF", + "isFromConfig": true + }, + "value": 2, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 15, + "propertyName": "Amber LED brightness level while the dimmer is ON", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 4, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Amber LED brightness level while the dimmer is ON", + "isFromConfig": true + }, + "value": 3, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 16, + "propertyName": "Amber LED brightness level while the dimmer is OFF", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 4, + "default": 2, + "format": 0, + "allowManualEntry": true, + "label": "Amber LED brightness level while the dimmer is OFF", + "isFromConfig": true + }, + "value": 0, + "ccVersion": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 11, + "propertyName": "Minimum Dimming Level", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 99, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Minimum Dimming Level", + "isFromConfig": true + }, + "value": 20, + "ccVersion": 1 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "manufacturerId", + "propertyName": "manufacturerId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 26, + "ccVersion": 2 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productType", + "propertyName": "productType", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 17481, + "ccVersion": 2 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productId", + "propertyName": "productId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 1281, + "ccVersion": 2 + }, + { + "commandClassName": "Protection", + "commandClass": 117, + "endpoint": 0, + "property": "local", + "propertyName": "local", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Local protection state", + "states": { + "0": "Unprotected", + "1": "ProtectedBySequence", + "2": "NoOperationPossible" + } + }, + "value": 0, + "ccVersion": 1 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "libraryType", + "propertyName": "libraryType", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3, + "ccVersion": 3 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "protocolVersion", + "propertyName": "protocolVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "5.3", + "ccVersion": 3 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": [ + "1.0" + ], + "ccVersion": 3 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + }, + "ccVersion": 3 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "sdkVersion", + "propertyName": "sdkVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "6.71.3", + "ccVersion": 3 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "applicationFrameworkAPIVersion", + "propertyName": "applicationFrameworkAPIVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "3.1.1", + "ccVersion": 3 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "applicationFrameworkBuildNumber", + "propertyName": "applicationFrameworkBuildNumber", + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 52445, + "ccVersion": 3 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "hostInterfaceVersion", + "propertyName": "hostInterfaceVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "unused", + "ccVersion": 3 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "hostInterfaceBuildNumber", + "propertyName": "hostInterfaceBuildNumber", + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 0, + "ccVersion": 3 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "zWaveProtocolVersion", + "propertyName": "zWaveProtocolVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "5.3.0", + "ccVersion": 3 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "zWaveProtocolBuildNumber", + "propertyName": "zWaveProtocolBuildNumber", + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 43, + "ccVersion": 3 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "applicationVersion", + "propertyName": "applicationVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "unused", + "ccVersion": 3 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "applicationBuildNumber", + "propertyName": "applicationBuildNumber", + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 0, + "ccVersion": 3 + } + ] +} diff --git a/tests/fixtures/zwave_js/ecolink_door_sensor_state.json b/tests/fixtures/zwave_js/ecolink_door_sensor_state.json new file mode 100644 index 00000000000..bd5f2c6b466 --- /dev/null +++ b/tests/fixtures/zwave_js/ecolink_door_sensor_state.json @@ -0,0 +1,334 @@ +{ + "nodeId": 53, + "index": 0, + "status": 1, + "ready": true, + "deviceClass": { + "basic": "Static Controller", + "generic": "Binary Sensor", + "specific": "Routing Binary Sensor", + "mandatorySupportedCCs": [ + "Basic", + "Binary Sensor" + ], + "mandatoryControlCCs": [ + + ] + }, + "isListening": false, + "isFrequentListening": false, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": false, + "version": 4, + "isBeaming": true, + "manufacturerId": 330, + "productId": 2, + "productType": 1, + "firmwareVersion": "2.0", + "deviceConfig": { + "manufacturerId": 330, + "manufacturer": "Ecolink", + "label": "DWZWAVE2", + "description": "Z-Wave Door/Window Sensor", + "devices": [ + { + "productType": "0x0001", + "productId": "0x0002" + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "associations": { + + }, + "paramInformation": { + "_map": { + + } + } + }, + "label": "DWZWAVE2", + "neighbors": [ + + ], + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 2, + "index": 0 + } + ], + "values": [ + { + "commandClassName": "Basic", + "commandClass": 32, + "endpoint": 0, + "property": "currentValue", + "propertyName": "currentValue", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 99, + "label": "Current value" + } + }, + { + "commandClassName": "Basic", + "commandClass": 32, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 99, + "label": "Target value" + } + }, + { + "commandClassName": "Binary Sensor", + "commandClass": 48, + "endpoint": 0, + "property": "Any", + "propertyName": "Any", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Any", + "ccSpecific": { + "sensorType": 255 + } + }, + "value": false + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 1, + "propertyName": "Sending Basic Sets to Association group 2", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": false, + "states": { + "0": "Off", + "255": "On" + }, + "label": "Sending Basic Sets to Association group 2", + "description": "Sending Basic Sets to Association group 2", + "isFromConfig": true + } + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 2, + "propertyName": "Sending sensor binary report", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": false, + "states": { + "0": "Off", + "255": "On" + }, + "label": "Sending sensor binary report", + "description": "Sending sensor binary report", + "isFromConfig": true + } + }, + { + "commandClassName": "Notification", + "commandClass": 113, + "endpoint": 0, + "property": "Home Security", + "propertyKey": "Cover status", + "propertyName": "Home Security", + "propertyKeyName": "Cover status", + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 3 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "manufacturerId", + "propertyName": "manufacturerId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 330 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productType", + "propertyName": "productType", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 1 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productId", + "propertyName": "productId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 2 + }, + { + "commandClassName": "Battery", + "commandClass": 128, + "endpoint": 0, + "property": "level", + "propertyName": "level", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 100, + "unit": "%", + "label": "Battery level" + }, + "value": 61 + }, + { + "commandClassName": "Battery", + "commandClass": 128, + "endpoint": 0, + "property": "isLow", + "propertyName": "isLow", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false + }, + { + "commandClassName": "Wake Up", + "commandClass": 132, + "endpoint": 0, + "property": "wakeUpInterval", + "propertyName": "wakeUpInterval", + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "min": 3600, + "max": 604800, + "label": "Wake Up interval", + "steps": 200, + "default": 14400 + }, + "value": 14400 + }, + { + "commandClassName": "Wake Up", + "commandClass": 132, + "endpoint": 0, + "property": "controllerNodeId", + "propertyName": "controllerNodeId", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Node ID of the controller" + }, + "value": 1 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "libraryType", + "propertyName": "libraryType", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 6 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "protocolVersion", + "propertyName": "protocolVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "3.40" + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": [ + "2.0" + ] + } + ] + } + \ No newline at end of file diff --git a/tests/fixtures/zwave_js/hank_binary_switch_state.json b/tests/fixtures/zwave_js/hank_binary_switch_state.json new file mode 100644 index 00000000000..0c629b3cf99 --- /dev/null +++ b/tests/fixtures/zwave_js/hank_binary_switch_state.json @@ -0,0 +1,727 @@ +{ + "nodeId": 32, + "index": 0, + "installerIcon": 1792, + "userIcon": 1792, + "status": 4, + "ready": true, + "deviceClass": { + "basic": "Static Controller", + "generic": "Binary Switch", + "specific": "Binary Power Switch", + "mandatorySupportedCCs": [ + "Basic", + "Binary Switch", + "All Switch" + ], + "mandatoryControlCCs": [] + }, + "isListening": true, + "isFrequentListening": false, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": false, + "version": 4, + "isBeaming": true, + "manufacturerId": 520, + "productId": 5, + "productType": 257, + "firmwareVersion": "1.5", + "zwavePlusVersion": 1, + "nodeType": 0, + "roleType": 5, + "deviceConfig": { + "manufacturerId": 520, + "manufacturer": "HANK Electronics Ltd.", + "label": "HKZW-SO01", + "description": "Smart Plug with two USB ports", + "devices": [ + { + "productType": "0x0101", + "productId": "0x0005" + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + } + }, + "label": "HKZW-SO01", + "neighbors": [ + 1, + 33, + 36, + 37, + 39, + 52 + ], + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 32, + "index": 0, + "installerIcon": 1792, + "userIcon": 1792 + } + ], + "values": [ + { + "commandClassName": "Binary Switch", + "commandClass": 37, + "endpoint": 0, + "property": "currentValue", + "propertyName": "currentValue", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value" + }, + "value": false + }, + { + "commandClassName": "Binary Switch", + "commandClass": 37, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value" + }, + "value": false + }, + { + "commandClassName": "Scene Activation", + "commandClass": 43, + "endpoint": 0, + "property": "sceneId", + "propertyName": "sceneId", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 1, + "max": 255, + "label": "Scene ID" + } + }, + { + "commandClassName": "Scene Activation", + "commandClass": 43, + "endpoint": 0, + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Dimming duration" + } + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "value", + "propertyKey": 66049, + "propertyName": "value", + "propertyKeyName": "W_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Value (Electric, Consumed)", + "unit": "W", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 2 + } + }, + "value": 0 + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "deltaTime", + "propertyKey": 66049, + "propertyName": "deltaTime", + "propertyKeyName": "W_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Time since the previous reading", + "unit": "s", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 2 + } + }, + "value": 0 + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "value", + "propertyKey": 65537, + "propertyName": "value", + "propertyKeyName": "kWh_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Value (Electric, Consumed)", + "unit": "kWh", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 0 + } + }, + "value": 0.164 + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "previousValue", + "propertyKey": 65537, + "propertyName": "previousValue", + "propertyKeyName": "kWh_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Previous value (Electric, Consumed)", + "unit": "kWh", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 0 + } + }, + "value": 0.164 + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "deltaTime", + "propertyKey": 65537, + "propertyName": "deltaTime", + "propertyKeyName": "kWh_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Time since the previous reading", + "unit": "s", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 0 + } + }, + "value": 30 + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "value", + "propertyKey": 66561, + "propertyName": "value", + "propertyKeyName": "V_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Value (Electric, Consumed)", + "unit": "V", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 4 + } + }, + "value": 122.963 + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "deltaTime", + "propertyKey": 66561, + "propertyName": "deltaTime", + "propertyKeyName": "V_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Time since the previous reading", + "unit": "s", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 4 + } + }, + "value": 0 + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "value", + "propertyKey": 66817, + "propertyName": "value", + "propertyKeyName": "A_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Value (Electric, Consumed)", + "unit": "A", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 5 + } + }, + "value": 0 + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "deltaTime", + "propertyKey": 66817, + "propertyName": "deltaTime", + "propertyKeyName": "A_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Time since the previous reading", + "unit": "s", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 5 + } + }, + "value": 0 + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "reset", + "propertyName": "reset", + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Reset accumulated values" + } + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "previousValue", + "propertyKey": 66049, + "propertyName": "previousValue", + "propertyKeyName": "W_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Previous value (Electric, Consumed)", + "unit": "W", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 2 + } + } + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "previousValue", + "propertyKey": 66561, + "propertyName": "previousValue", + "propertyKeyName": "V_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Previous value (Electric, Consumed)", + "unit": "V", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 4 + } + } + }, + { + "commandClassName": "Meter", + "commandClass": 50, + "endpoint": 0, + "property": "previousValue", + "propertyKey": 66817, + "propertyName": "previousValue", + "propertyKeyName": "A_Consumed", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Previous value (Electric, Consumed)", + "unit": "A", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 5 + } + } + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 20, + "propertyName": "Overload Protection", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Overload Protection", + "description": "If current exceeds 16.5A over 5 seconds, relay will turn off.", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 21, + "propertyName": "Device Status after Power Failure", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 2, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Device Status after Power Failure", + "description": "Define how the plug reacts after power failure", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 24, + "propertyName": "Notifcation on Load Change", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 2, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Notifcation on Load Change", + "description": "Smart Plug can send notifications to association device load state changes.", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 27, + "propertyName": "Indicator Modes", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Indicator Modes", + "description": "LED in the device will indicate the state of load", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 151, + "propertyName": "Threshold of power report", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 0, + "max": 65535, + "default": 50, + "format": 1, + "allowManualEntry": true, + "label": "Threshold of power report", + "description": "Power Threshold at which to send meter report", + "isFromConfig": true + }, + "value": 50 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 152, + "propertyName": "Percentage Threshold of Power Report", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 10, + "format": 1, + "allowManualEntry": true, + "label": "Percentage Threshold of Power Report", + "description": "Percentage Threshold at which to send meter report", + "isFromConfig": true + }, + "value": 10 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 171, + "propertyName": "Power Report Frequency", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 5, + "max": 2678400, + "default": 30, + "format": 0, + "allowManualEntry": true, + "label": "Power Report Frequency", + "description": "The interval of sending power report to association device (Group Lifeline).", + "isFromConfig": true + }, + "value": 30 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 172, + "propertyName": "Energy Report Frequency", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 5, + "max": 2678400, + "default": 300, + "format": 0, + "allowManualEntry": true, + "label": "Energy Report Frequency", + "description": "The interval of sending energy report to association device (Group Lifeline).", + "isFromConfig": true + }, + "value": 300 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 173, + "propertyName": "Voltage Report Frequency", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 2678400, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Voltage Report Frequency", + "description": "The interval of sending voltage report to association device (Group Lifeline)", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 174, + "propertyName": "Electricity Report Frequency", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 2678400, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Electricity Report Frequency", + "description": "Interval for sending electricity report.", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "manufacturerId", + "propertyName": "manufacturerId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 520 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productType", + "propertyName": "productType", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 257 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productId", + "propertyName": "productId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 5 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "libraryType", + "propertyName": "libraryType", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Libary type" + }, + "value": 3 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "protocolVersion", + "propertyName": "protocolVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.24" + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": [ + "1.5" + ] + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/zwave_js/in_wall_smart_fan_control_state.json b/tests/fixtures/zwave_js/in_wall_smart_fan_control_state.json new file mode 100644 index 00000000000..fe5550a5424 --- /dev/null +++ b/tests/fixtures/zwave_js/in_wall_smart_fan_control_state.json @@ -0,0 +1,354 @@ +{ + "nodeId": 17, + "index": 0, + "installerIcon": 1024, + "userIcon": 1024, + "status": 4, + "ready": true, + "deviceClass": { + "basic": "Routing Slave", + "generic": "Multilevel Switch", + "specific": "Fan Switch", + "mandatorySupportedCCs": [ + "Basic", + "Multilevel Switch" + ], + "mandatoryControlCCs": [] + }, + "isListening": true, + "isFrequentListening": false, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": false, + "version": 4, + "isBeaming": true, + "manufacturerId": 99, + "productId": 12593, + "productType": 18756, + "firmwareVersion": "5.22", + "zwavePlusVersion": 1, + "nodeType": 0, + "roleType": 5, + "deviceConfig": { + "manufacturerId": 99, + "manufacturer": "GE/Jasco", + "label": "ZW4002", + "description": "In-Wall Smart Fan Control", + "devices": [ + { + "productType": "0x4944", + "productId": "0x3131" + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "associations": {}, + "paramInformation": { + "_map": {} + } + }, + "label": "ZW4002", + "neighbors": [ + 1, + 2, + 6, + 8, + 9, + 10, + 11, + 14, + 15, + 16, + 18, + 19, + 20, + 21, + 23, + 26, + 27, + 30, + 31, + 33, + 36, + 37, + 38, + 39, + 41, + 42 + ], + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 17, + "index": 0, + "installerIcon": 1024, + "userIcon": 1024 + } + ], + "values": [ + { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 99, + "label": "Target value" + } + }, + { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "duration", + "propertyName": "duration", + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Transition duration" + } + }, + { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "currentValue", + "propertyName": "currentValue", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 99, + "label": "Current value" + }, + "value": 0 + }, + { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "Up", + "propertyName": "Up", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { + "switchType": 2 + } + } + }, + { + "commandClassName": "Multilevel Switch", + "commandClass": 38, + "endpoint": 0, + "property": "Down", + "propertyName": "Down", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { + "switchType": 2 + } + } + }, + { + "commandClassName": "Scene Activation", + "commandClass": 43, + "endpoint": 0, + "property": "sceneId", + "propertyName": "sceneId", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 1, + "max": 255, + "label": "Scene ID" + } + }, + { + "commandClassName": "Scene Activation", + "commandClass": 43, + "endpoint": 0, + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Dimming duration" + } + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 3, + "propertyName": "Night Light", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": false, + "states": { + "0": "LED on when switch is OFF", + "1": "LED on when switch is ON", + "2": "LED always off" + }, + "label": "Night Light", + "description": "Defines the behavior of the blue LED", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 4, + "propertyName": "Invert Switch", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": false, + "states": { + "0": "No", + "1": "Yes" + }, + "label": "Invert Switch", + "description": "Invert the ON/OFF Switch State", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "manufacturerId", + "propertyName": "manufacturerId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 99 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productType", + "propertyName": "productType", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 18756 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productId", + "propertyName": "productId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 12593 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "libraryType", + "propertyName": "libraryType", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "protocolVersion", + "propertyName": "protocolVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.54" + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": [ + "5.22" + ] + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + } + ] + } \ No newline at end of file diff --git a/tests/fixtures/zwave_js/lock_august_asl03_state.json b/tests/fixtures/zwave_js/lock_august_asl03_state.json new file mode 100644 index 00000000000..b6d44341853 --- /dev/null +++ b/tests/fixtures/zwave_js/lock_august_asl03_state.json @@ -0,0 +1,456 @@ +{ + "nodeId": 6, + "index": 0, + "installerIcon": 768, + "userIcon": 768, + "status": 4, + "ready": true, + "deviceClass": { + "basic": "Routing Slave", + "generic": "Entry Control", + "specific": "Secure Keypad Door Lock", + "mandatorySupportedCCs": [ + "Basic", + "Door Lock", + "User Code", + "Manufacturer Specific", + "Security", + "Version" + ], + "mandatoryControlCCs": [] + }, + "isListening": false, + "isFrequentListening": true, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": true, + "version": 4, + "isBeaming": true, + "manufacturerId": 831, + "productId": 1, + "productType": 1, + "firmwareVersion": "1.59", + "zwavePlusVersion": 1, + "nodeType": 0, + "roleType": 7, + "deviceConfig": { + "manufacturerId": 831, + "manufacturer": "August Smart Locks", + "label": "ASL-03", + "description": "August Smart Lock Pro 3rd Gen", + "devices": [ + { + "productType": "0x0000", + "productId": "0x0594" + }, + { + "productType": "0x0000", + "productId": "0xdf29" + }, + { + "productType": "0x0001", + "productId": "0x0001" + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + } + }, + "label": "ASL-03", + "neighbors": [ + 1, + 7, + 8, + 9 + ], + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 6, + "index": 0, + "installerIcon": 768, + "userIcon": 768 + } + ], + "values": [ + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "currentMode", + "propertyName": "currentMode", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Current lock mode", + "states": { + "0": "Unsecured", + "1": "UnsecuredWithTimeout", + "16": "InsideUnsecured", + "17": "InsideUnsecuredWithTimeout", + "32": "OutsideUnsecured", + "33": "OutsideUnsecuredWithTimeout", + "254": "Unknown", + "255": "Secured" + } + }, + "value": 255 + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "targetMode", + "propertyName": "targetMode", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 255, + "label": "Target lock mode", + "states": { + "0": "Unsecured", + "1": "UnsecuredWithTimeout", + "16": "InsideUnsecured", + "17": "InsideUnsecuredWithTimeout", + "32": "OutsideUnsecured", + "33": "OutsideUnsecuredWithTimeout", + "254": "Unknown", + "255": "Secured" + } + } + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "outsideHandlesCanOpenDoor", + "propertyName": "outsideHandlesCanOpenDoor", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Which outside handles can open the door (actual status)" + }, + "value": [ + false, + false, + false, + false + ] + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "insideHandlesCanOpenDoor", + "propertyName": "insideHandlesCanOpenDoor", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Which inside handles can open the door (actual status)" + }, + "value": [ + true, + false, + false, + false + ] + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "latchStatus", + "propertyName": "latchStatus", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the latch" + }, + "value": "open" + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "boltStatus", + "propertyName": "boltStatus", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the bolt" + }, + "value": "locked" + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "doorStatus", + "propertyName": "doorStatus", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the door" + }, + "value": "closed" + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "lockTimeout", + "propertyName": "lockTimeout", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Seconds until lock mode times out" + } + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "operationType", + "propertyName": "operationType", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 255, + "label": "Lock operation type", + "states": { + "1": "Constant", + "2": "Timed" + } + }, + "value": 1 + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "outsideHandlesCanOpenDoorConfiguration", + "propertyName": "outsideHandlesCanOpenDoorConfiguration", + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Which outside handles can open the door (configuration)" + }, + "value": [ + false, + false, + false, + false + ] + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "insideHandlesCanOpenDoorConfiguration", + "propertyName": "insideHandlesCanOpenDoorConfiguration", + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Which inside handles can open the door (configuration)" + }, + "value": [ + true, + false, + false, + false + ] + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "lockTimeoutConfiguration", + "propertyName": "lockTimeoutConfiguration", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 65535, + "label": "Duration of timed mode in seconds" + } + }, + { + "commandClassName": "Notification", + "commandClass": 113, + "endpoint": 0, + "property": "Access Control", + "propertyKey": "Lock state", + "propertyName": "Access Control", + "propertyKeyName": "Lock state", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Lock state", + "states": { + "0": "idle", + "11": "Lock jammed" + }, + "ccSpecific": { + "notificationType": 6 + } + }, + "value": 0 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "manufacturerId", + "propertyName": "manufacturerId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 831 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productType", + "propertyName": "productType", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 1 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productId", + "propertyName": "productId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 1 + }, + { + "commandClassName": "Battery", + "commandClass": 128, + "endpoint": 0, + "property": "level", + "propertyName": "level", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 100, + "unit": "%", + "label": "Battery level" + }, + "value": 100 + }, + { + "commandClassName": "Battery", + "commandClass": 128, + "endpoint": 0, + "property": "isLow", + "propertyName": "isLow", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "libraryType", + "propertyName": "libraryType", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "protocolVersion", + "propertyName": "protocolVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.61" + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": [ + "1.59" + ] + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/zwave_js/lock_schlage_be469_state.json b/tests/fixtures/zwave_js/lock_schlage_be469_state.json new file mode 100644 index 00000000000..af1fc92a206 --- /dev/null +++ b/tests/fixtures/zwave_js/lock_schlage_be469_state.json @@ -0,0 +1,2052 @@ +{ + "nodeId": 20, + "index": 0, + "status": 4, + "ready": true, + "deviceClass": { + "basic": "Static Controller", + "generic": "Entry Control", + "specific": "Secure Keypad Door Lock", + "mandatorySupportedCCs": [ + "Basic", + "Door Lock", + "User Code", + "Manufacturer Specific", + "Security", + "Version" + ], + "mandatoryControlCCs": [] + }, + "isListening": false, + "isFrequentListening": true, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": true, + "version": 4, + "isBeaming": true, + "manufacturerId": 59, + "productId": 20548, + "productType": 25409, + "firmwareVersion": "113.22", + "deviceConfig": { + "manufacturerId": 59, + "manufacturer": "Allegion", + "label": "BE469", + "description": "Touchscreen Deadbolt", + "devices": [ + { + "productType": "0x6341", + "productId": "0x5044" + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "associations": {}, + "paramInformation": { + "_map": {} + } + }, + "label": "BE469", + "neighbors": [1, 2, 3, 4, 13], + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 20, + "index": 0 + } + ], + "values": [ + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "currentMode", + "propertyName": "currentMode", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Current lock mode", + "states": { + "0": "Unsecured", + "1": "UnsecuredWithTimeout", + "16": "InsideUnsecured", + "17": "InsideUnsecuredWithTimeout", + "32": "OutsideUnsecured", + "33": "OutsideUnsecuredWithTimeout", + "254": "Unknown", + "255": "Secured" + } + }, + "value": 0 + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "targetMode", + "propertyName": "targetMode", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 255, + "label": "Target lock mode", + "states": { + "0": "Unsecured", + "1": "UnsecuredWithTimeout", + "16": "InsideUnsecured", + "17": "InsideUnsecuredWithTimeout", + "32": "OutsideUnsecured", + "33": "OutsideUnsecuredWithTimeout", + "254": "Unknown", + "255": "Secured" + } + } + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "outsideHandlesCanOpenDoor", + "propertyName": "outsideHandlesCanOpenDoor", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Which outside handles can open the door (actual status)" + }, + "value": [false, false, false, false] + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "insideHandlesCanOpenDoor", + "propertyName": "insideHandlesCanOpenDoor", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Which inside handles can open the door (actual status)" + }, + "value": [false, false, false, false] + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "latchStatus", + "propertyName": "latchStatus", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the latch" + }, + "value": "open" + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "boltStatus", + "propertyName": "boltStatus", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the bolt" + }, + "value": "unlocked" + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "doorStatus", + "propertyName": "doorStatus", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "The current status of the door" + }, + "value": "open" + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "lockTimeout", + "propertyName": "lockTimeout", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Seconds until lock mode times out" + } + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "operationType", + "propertyName": "operationType", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 255, + "label": "Lock operation type", + "states": { + "1": "Constant", + "2": "Timed" + } + }, + "value": 1 + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "outsideHandlesCanOpenDoorConfiguration", + "propertyName": "outsideHandlesCanOpenDoorConfiguration", + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Which outside handles can open the door (configuration)" + }, + "value": [false, false, false, false] + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "insideHandlesCanOpenDoorConfiguration", + "propertyName": "insideHandlesCanOpenDoorConfiguration", + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Which inside handles can open the door (configuration)" + }, + "value": [false, false, false, false] + }, + { + "commandClassName": "Door Lock", + "commandClass": 98, + "endpoint": 0, + "property": "lockTimeoutConfiguration", + "propertyName": "lockTimeoutConfiguration", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 65535, + "label": "Duration of timed mode in seconds" + } + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyKey": 1, + "propertyName": "userIdStatus", + "propertyKeyName": "1", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (1)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 1 + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyKey": 1, + "propertyName": "userCode", + "propertyKeyName": "1", + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "minLength": 4, + "maxLength": 10, + "label": "User Code (1)" + }, + "value": "**********" + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyKey": 2, + "propertyName": "userIdStatus", + "propertyKeyName": "2", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (2)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 1 + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyKey": 2, + "propertyName": "userCode", + "propertyKeyName": "2", + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "minLength": 4, + "maxLength": 10, + "label": "User Code (2)" + }, + "value": "**********" + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyKey": 3, + "propertyName": "userIdStatus", + "propertyKeyName": "3", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (3)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 1 + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyKey": 3, + "propertyName": "userCode", + "propertyKeyName": "3", + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "minLength": 4, + "maxLength": 10, + "label": "User Code (3)" + }, + "value": "**********" + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyKey": 4, + "propertyName": "userIdStatus", + "propertyKeyName": "4", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (4)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyKey": 4, + "propertyName": "userCode", + "propertyKeyName": "4", + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "minLength": 4, + "maxLength": 10, + "label": "User Code (4)" + }, + "value": "" + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyKey": 5, + "propertyName": "userIdStatus", + "propertyKeyName": "5", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (5)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyKey": 5, + "propertyName": "userCode", + "propertyKeyName": "5", + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "minLength": 4, + "maxLength": 10, + "label": "User Code (5)" + }, + "value": "" + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyKey": 6, + "propertyName": "userIdStatus", + "propertyKeyName": "6", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (6)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyKey": 6, + "propertyName": "userCode", + "propertyKeyName": "6", + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "minLength": 4, + "maxLength": 10, + "label": "User Code (6)" + }, + "value": "" + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyKey": 7, + "propertyName": "userIdStatus", + "propertyKeyName": "7", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (7)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyKey": 7, + "propertyName": "userCode", + "propertyKeyName": "7", + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "minLength": 4, + "maxLength": 10, + "label": "User Code (7)" + }, + "value": "" + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyKey": 8, + "propertyName": "userIdStatus", + "propertyKeyName": "8", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (8)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyKey": 8, + "propertyName": "userCode", + "propertyKeyName": "8", + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "minLength": 4, + "maxLength": 10, + "label": "User Code (8)" + }, + "value": "" + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyKey": 9, + "propertyName": "userIdStatus", + "propertyKeyName": "9", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (9)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyKey": 9, + "propertyName": "userCode", + "propertyKeyName": "9", + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "minLength": 4, + "maxLength": 10, + "label": "User Code (9)" + }, + "value": "" + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyKey": 10, + "propertyName": "userIdStatus", + "propertyKeyName": "10", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (10)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyKey": 10, + "propertyName": "userCode", + "propertyKeyName": "10", + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "minLength": 4, + "maxLength": 10, + "label": "User Code (10)" + }, + "value": "" + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyKey": 11, + "propertyName": "userIdStatus", + "propertyKeyName": "11", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (11)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyKey": 11, + "propertyName": "userCode", + "propertyKeyName": "11", + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "minLength": 4, + "maxLength": 10, + "label": "User Code (11)" + }, + "value": "" + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyKey": 12, + "propertyName": "userIdStatus", + "propertyKeyName": "12", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (12)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyKey": 12, + "propertyName": "userCode", + "propertyKeyName": "12", + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "minLength": 4, + "maxLength": 10, + "label": "User Code (12)" + }, + "value": "" + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyKey": 13, + "propertyName": "userIdStatus", + "propertyKeyName": "13", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (13)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyKey": 13, + "propertyName": "userCode", + "propertyKeyName": "13", + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "minLength": 4, + "maxLength": 10, + "label": "User Code (13)" + }, + "value": "" + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyKey": 14, + "propertyName": "userIdStatus", + "propertyKeyName": "14", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (14)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyKey": 14, + "propertyName": "userCode", + "propertyKeyName": "14", + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "minLength": 4, + "maxLength": 10, + "label": "User Code (14)" + }, + "value": "" + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyKey": 15, + "propertyName": "userIdStatus", + "propertyKeyName": "15", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (15)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyKey": 15, + "propertyName": "userCode", + "propertyKeyName": "15", + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "minLength": 4, + "maxLength": 10, + "label": "User Code (15)" + }, + "value": "" + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyKey": 16, + "propertyName": "userIdStatus", + "propertyKeyName": "16", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (16)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyKey": 16, + "propertyName": "userCode", + "propertyKeyName": "16", + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "minLength": 4, + "maxLength": 10, + "label": "User Code (16)" + }, + "value": "" + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyKey": 17, + "propertyName": "userIdStatus", + "propertyKeyName": "17", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (17)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyKey": 17, + "propertyName": "userCode", + "propertyKeyName": "17", + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "minLength": 4, + "maxLength": 10, + "label": "User Code (17)" + }, + "value": "" + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyKey": 18, + "propertyName": "userIdStatus", + "propertyKeyName": "18", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (18)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyKey": 18, + "propertyName": "userCode", + "propertyKeyName": "18", + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "minLength": 4, + "maxLength": 10, + "label": "User Code (18)" + }, + "value": "" + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyKey": 19, + "propertyName": "userIdStatus", + "propertyKeyName": "19", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (19)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyKey": 19, + "propertyName": "userCode", + "propertyKeyName": "19", + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "minLength": 4, + "maxLength": 10, + "label": "User Code (19)" + }, + "value": "" + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyKey": 20, + "propertyName": "userIdStatus", + "propertyKeyName": "20", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (20)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyKey": 20, + "propertyName": "userCode", + "propertyKeyName": "20", + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "minLength": 4, + "maxLength": 10, + "label": "User Code (20)" + }, + "value": "" + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyKey": 21, + "propertyName": "userIdStatus", + "propertyKeyName": "21", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (21)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyKey": 21, + "propertyName": "userCode", + "propertyKeyName": "21", + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "minLength": 4, + "maxLength": 10, + "label": "User Code (21)" + }, + "value": "" + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyKey": 22, + "propertyName": "userIdStatus", + "propertyKeyName": "22", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (22)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyKey": 22, + "propertyName": "userCode", + "propertyKeyName": "22", + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "minLength": 4, + "maxLength": 10, + "label": "User Code (22)" + }, + "value": "" + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyKey": 23, + "propertyName": "userIdStatus", + "propertyKeyName": "23", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (23)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyKey": 23, + "propertyName": "userCode", + "propertyKeyName": "23", + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "minLength": 4, + "maxLength": 10, + "label": "User Code (23)" + }, + "value": "" + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyKey": 24, + "propertyName": "userIdStatus", + "propertyKeyName": "24", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (24)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyKey": 24, + "propertyName": "userCode", + "propertyKeyName": "24", + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "minLength": 4, + "maxLength": 10, + "label": "User Code (24)" + }, + "value": "" + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyKey": 25, + "propertyName": "userIdStatus", + "propertyKeyName": "25", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (25)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyKey": 25, + "propertyName": "userCode", + "propertyKeyName": "25", + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "minLength": 4, + "maxLength": 10, + "label": "User Code (25)" + }, + "value": "" + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyKey": 26, + "propertyName": "userIdStatus", + "propertyKeyName": "26", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (26)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyKey": 26, + "propertyName": "userCode", + "propertyKeyName": "26", + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "minLength": 4, + "maxLength": 10, + "label": "User Code (26)" + }, + "value": "" + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyKey": 27, + "propertyName": "userIdStatus", + "propertyKeyName": "27", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (27)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyKey": 27, + "propertyName": "userCode", + "propertyKeyName": "27", + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "minLength": 4, + "maxLength": 10, + "label": "User Code (27)" + }, + "value": "" + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyKey": 28, + "propertyName": "userIdStatus", + "propertyKeyName": "28", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (28)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyKey": 28, + "propertyName": "userCode", + "propertyKeyName": "28", + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "minLength": 4, + "maxLength": 10, + "label": "User Code (28)" + }, + "value": "" + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyKey": 29, + "propertyName": "userIdStatus", + "propertyKeyName": "29", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (29)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyKey": 29, + "propertyName": "userCode", + "propertyKeyName": "29", + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "minLength": 4, + "maxLength": 10, + "label": "User Code (29)" + }, + "value": "" + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userIdStatus", + "propertyKey": 30, + "propertyName": "userIdStatus", + "propertyKeyName": "30", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "User ID status (30)", + "states": { + "0": "Available", + "1": "Enabled", + "2": "Disabled" + } + }, + "value": 0 + }, + { + "commandClassName": "User Code", + "commandClass": 99, + "endpoint": 0, + "property": "userCode", + "propertyKey": 30, + "propertyName": "userCode", + "propertyKeyName": "30", + "metadata": { + "type": "string", + "readable": true, + "writeable": true, + "minLength": 4, + "maxLength": 10, + "label": "User Code (30)" + }, + "value": "" + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "manufacturerId", + "propertyName": "manufacturerId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 59 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productType", + "propertyName": "productType", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 25409 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productId", + "propertyName": "productId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 20548 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "libraryType", + "propertyName": "libraryType", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 6 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "protocolVersion", + "propertyName": "protocolVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "3.42" + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["113.22"] + }, + { + "commandClassName": "Battery", + "commandClass": 128, + "endpoint": 0, + "property": "level", + "propertyName": "level", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 100, + "unit": "%", + "label": "Battery level" + }, + "value": 100 + }, + { + "commandClassName": "Battery", + "commandClass": 128, + "endpoint": 0, + "property": "isLow", + "propertyName": "isLow", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 3, + "propertyName": "Beeper", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 255, + "format": 1, + "allowManualEntry": false, + "states": { + "0": "Disable Beeper", + "255": "Enable Beeper" + }, + "label": "Beeper", + "isFromConfig": true + }, + "value": 255 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 4, + "propertyName": "Vacation Mode", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": false, + "states": { + "0": "Disable Vacation Mode", + "255": "Enable Vacation Mode" + }, + "label": "Vacation Mode", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 5, + "propertyName": "Lock & Leave", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": false, + "states": { + "0": "Disable Lock & Leave", + "255": "Enable Lock & Leave" + }, + "label": "Lock & Leave", + "isFromConfig": true + }, + "value": 255 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 6, + "propertyName": "User Slot Status", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "valueSize": 4, + "min": 0, + "max": 255, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "User Slot Status", + "description": "User slot status", + "isFromConfig": true + }, + "value": 117440512 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 7, + "propertyName": "Lock Specific Alarm Mode", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 3, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Alarm Off", + "1": "Alert", + "2": "Tamper", + "3": "Forced Entry" + }, + "label": "Lock Specific Alarm Mode", + "description": "BE469 Only", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 8, + "propertyName": "Lock Specific Alarm Alert Sensitivity", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 5, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Not Supported", + "1": "Most Sensitive", + "2": "More Sensitive", + "3": "Medium Sensitivity", + "4": "Less Sensitive", + "5": "Least Sensitive" + }, + "label": "Lock Specific Alarm Alert Sensitivity", + "isFromConfig": true + }, + "value": 3 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 9, + "propertyName": "Lock Specific Alarm Tamper Sensitivity", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 5, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Not Supported", + "1": "Most Sensitive", + "2": "More Sensitive", + "3": "Medium Sensitivity", + "4": "Less Sensitive", + "5": "Least Sensitive" + }, + "label": "Lock Specific Alarm Tamper Sensitivity", + "isFromConfig": true + }, + "value": 3 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 10, + "propertyName": "Lock Specific Alarm Kick Sensitivity", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 5, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Not Supported", + "1": "Most Sensitive", + "2": "More Sensitive", + "3": "Medium Sensitivity", + "4": "Less Sensitive", + "5": "Least Sensitive" + }, + "label": "Lock Specific Alarm Kick Sensitivity", + "description": "BE469 Only", + "isFromConfig": true + }, + "value": 3 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 11, + "propertyName": "Lock Specific Alarm Disable—Local Controls", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": false, + "states": { + "0": "Disable Local Control", + "255": "Enable Local Control" + }, + "label": "Lock Specific Alarm Disable—Local Controls", + "isFromConfig": true + }, + "value": 255 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 12, + "propertyName": "Electronic Transition Count", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "valueSize": 4, + "min": 0, + "max": 2147483647, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Electronic Transition Count", + "isFromConfig": true + }, + "value": 2260 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 13, + "propertyName": "Mechanical Transition Count", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "valueSize": 4, + "min": 0, + "max": 2147483647, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Mechanical Transition Count", + "isFromConfig": true + }, + "value": 2166 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 14, + "propertyName": "Electronic Failed Count", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "valueSize": 4, + "min": 0, + "max": 2147483647, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Electronic Failed Count", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 15, + "propertyName": "Auto Lock", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": false, + "states": { + "0": "Disable Auto Lock", + "255": "Enable Auto Lock" + }, + "label": "Auto Lock", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 16, + "propertyName": "User Code PIN Length", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 4, + "max": 8, + "default": 4, + "format": 0, + "allowManualEntry": false, + "states": { + "4": "Four Digits", + "5": "Five Digits", + "6": "Six Digits", + "7": "Seven Digits", + "8": "Eight Digits" + }, + "label": "User Code PIN Length", + "description": "User Code PIN length, a value between 4 and 8 (default 4)", + "isFromConfig": true + }, + "value": 4 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 18, + "propertyName": "Get Bootloader Version", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": true, + "label": "Get Bootloader Version", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Notification", + "commandClass": 113, + "endpoint": 0, + "property": "Access Control", + "propertyKey": "Lock state", + "propertyName": "Access Control", + "propertyKeyName": "Lock state", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Lock state", + "states": { + "0": "idle", + "11": "Lock jammed" + }, + "ccSpecific": { + "notificationType": 6 + } + }, + "value": 0 + }, + { + "commandClassName": "Notification", + "commandClass": 113, + "endpoint": 0, + "property": "Access Control", + "propertyKey": "Keypad state", + "propertyName": "Access Control", + "propertyKeyName": "Keypad state", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Keypad state", + "states": { + "0": "idle", + "16": "Keypad temporary disabled" + }, + "ccSpecific": { + "notificationType": 6 + } + }, + "value": 0 + }, + { + "commandClassName": "Notification", + "commandClass": 113, + "endpoint": 0, + "property": "Home Security", + "propertyKey": "Sensor status", + "propertyName": "Home Security", + "propertyKeyName": "Sensor status", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Sensor status", + "states": { + "0": "idle", + "2": "Intrusion" + }, + "ccSpecific": { + "notificationType": 7 + } + }, + "value": 0 + }, + { + "commandClassName": "Notification", + "commandClass": 113, + "endpoint": 0, + "property": "Power Management", + "propertyKey": "Battery maintenance status", + "propertyName": "Power Management", + "propertyKeyName": "Battery maintenance status", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Battery maintenance status", + "states": { + "0": "idle", + "10": "Replace battery soon", + "11": "Replace battery now" + }, + "ccSpecific": { + "notificationType": 8 + } + }, + "value": 0 + }, + { + "commandClassName": "Notification", + "commandClass": 113, + "endpoint": 0, + "property": "System", + "propertyKey": "Hardware status", + "propertyName": "System", + "propertyKeyName": "Hardware status", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Hardware status", + "states": { + "0": "idle", + "1": "System hardware failure" + }, + "ccSpecific": { + "notificationType": 9 + } + }, + "value": 0 + } + ] +} diff --git a/tests/fixtures/zwave_js/multisensor_6_state.json b/tests/fixtures/zwave_js/multisensor_6_state.json new file mode 100644 index 00000000000..3c508ffd3ff --- /dev/null +++ b/tests/fixtures/zwave_js/multisensor_6_state.json @@ -0,0 +1,1830 @@ +{ + "nodeId": 52, + "index": 0, + "installerIcon": 3079, + "userIcon": 3079, + "status": 1, + "ready": true, + "deviceClass": { + "basic": "Static Controller", + "generic": "Multilevel Sensor", + "specific": "Routing Multilevel Sensor", + "mandatorySupportedCCs": [ + "Basic", + "Multilevel Sensor" + ], + "mandatoryControlCCs": [] + }, + "isListening": true, + "isFrequentListening": false, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": false, + "version": 4, + "isBeaming": true, + "manufacturerId": 134, + "productId": 100, + "productType": 258, + "firmwareVersion": "1.12", + "zwavePlusVersion": 1, + "nodeType": 0, + "roleType": 5, + "deviceConfig": { + "manufacturerId": 134, + "manufacturer": "AEON Labs", + "label": "ZW100", + "description": "Multisensor 6", + "devices": [ + { + "productType": "0x0002", + "productId": "0x0064" + }, + { + "productType": "0x0102", + "productId": "0x0064" + }, + { + "productType": "0x0202", + "productId": "0x0064" + } + ], + "firmwareVersion": { + "min": "1.10", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + } + }, + "label": "ZW100", + "neighbors": [ + 1, + 32 + ], + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 52, + "index": 0, + "installerIcon": 3079, + "userIcon": 3079 + } + ], + "values": [ + { + "commandClassName": "Basic", + "commandClass": 32, + "endpoint": 0, + "property": "currentValue", + "propertyName": "currentValue", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 99, + "label": "Current value" + }, + "value": 255 + }, + { + "commandClassName": "Basic", + "commandClass": 32, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 99, + "label": "Target value" + } + }, + { + "commandClassName": "Binary Sensor", + "commandClass": 48, + "endpoint": 0, + "property": "Any", + "propertyName": "Any", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Any", + "ccSpecific": { + "sensorType": 255 + } + }, + "value": false + }, + { + "commandClassName": "Multilevel Sensor", + "commandClass": 49, + "endpoint": 0, + "property": "Air temperature", + "propertyName": "Air temperature", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "unit": "°C", + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 0 + } + }, + "value": 9 + }, + { + "commandClassName": "Multilevel Sensor", + "commandClass": 49, + "endpoint": 0, + "property": "Illuminance", + "propertyName": "Illuminance", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "unit": "Lux", + "label": "Illuminance", + "ccSpecific": { + "sensorType": 3, + "scale": 1 + } + }, + "value": 0 + }, + { + "commandClassName": "Multilevel Sensor", + "commandClass": 49, + "endpoint": 0, + "property": "Humidity", + "propertyName": "Humidity", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "unit": "%", + "label": "Humidity", + "ccSpecific": { + "sensorType": 5, + "scale": 0 + } + }, + "value": 65 + }, + { + "commandClassName": "Multilevel Sensor", + "commandClass": 49, + "endpoint": 0, + "property": "Ultraviolet", + "propertyName": "Ultraviolet", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Ultraviolet", + "ccSpecific": { + "sensorType": 27, + "scale": 0 + } + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 2, + "propertyName": "Stay Awake in Battery Mode", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Disable", + "1": "Enable" + }, + "label": "Stay Awake in Battery Mode", + "description": "Stay awake for 10 minutes at power on", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 3, + "propertyName": "Motion Sensor reset timeout", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 10, + "max": 3600, + "default": 240, + "format": 0, + "allowManualEntry": true, + "label": "Motion Sensor reset timeout", + "description": "Motion Sensor reset timeout", + "isFromConfig": true + }, + "value": 240 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 4, + "propertyName": "Motion sensor sensitivity", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 5, + "format": 1, + "allowManualEntry": false, + "states": { + "0": "Disable", + "1": "Enable, sensitivity level 1 (minimum)", + "2": "Enable, sensitivity level 2", + "3": "Enable, sensitivity level 3", + "4": "Enable, sensitivity level 4", + "5": "Enable, sensitivity level 5 (maximum)" + }, + "label": "Motion sensor sensitivity", + "description": "Sensitivity level of PIR sensor (1=minimum, 5=maximum)", + "isFromConfig": true + }, + "value": 5 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 5, + "propertyName": "Motion Sensor Triggered Command", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 1, + "format": 1, + "allowManualEntry": false, + "states": { + "1": "Send Basic Set CC", + "2": "Send Sensor Binary Report CC" + }, + "label": "Motion Sensor Triggered Command", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 8, + "propertyName": "Timeout after wake up", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 8, + "max": 255, + "default": 30, + "format": 1, + "allowManualEntry": true, + "label": "Timeout after wake up", + "description": "Set the timeout of awake after the Wake Up CC is sent out...", + "isFromConfig": true + }, + "value": 15 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 39, + "propertyName": "Low Battery Report", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 10, + "max": 50, + "default": 20, + "format": 0, + "allowManualEntry": true, + "label": "Low Battery Report", + "description": "Report Low Battery if below this value", + "isFromConfig": true + }, + "value": 20 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 40, + "propertyName": "Selective Reporting", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Disable", + "1": "Enable" + }, + "label": "Selective Reporting", + "description": "Select to report on thresholds", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 42, + "propertyName": "Humidity Threshold", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 10, + "format": 0, + "allowManualEntry": true, + "label": "Humidity Threshold", + "description": "Humidity percent change threshold", + "isFromConfig": true + }, + "value": 10 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 43, + "propertyName": "Luminance Threshold", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 0, + "max": 1000, + "default": 100, + "format": 0, + "allowManualEntry": true, + "label": "Luminance Threshold", + "description": "Luminance change threshold", + "isFromConfig": true + }, + "value": 100 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 44, + "propertyName": "Battery Threshold", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 10, + "format": 0, + "allowManualEntry": true, + "label": "Battery Threshold", + "description": "Battery level threshold", + "isFromConfig": true + }, + "value": 10 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 45, + "propertyName": "Ultraviolet Threshold", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 2, + "format": 0, + "allowManualEntry": true, + "label": "Ultraviolet Threshold", + "description": "Ultraviolet change threshold", + "isFromConfig": true + }, + "value": 2 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 46, + "propertyName": "Send Alarm Report if low temperature", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": false, + "states": { + "0": "Disable", + "1": "Enable" + }, + "label": "Send Alarm Report if low temperature", + "description": "Send an alarm report if temperature is less than -15 °C", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 48, + "propertyName": "Send a report if the measurement is out of limits", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": true, + "label": "Send a report if the measurement is out of limits", + "description": "Send report when measurement is at upper/lower limit", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 51, + "propertyName": "Upper limit value of humidity sensor", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 60, + "format": 0, + "allowManualEntry": true, + "label": "Upper limit value of humidity sensor", + "description": "Upper limit value of humidity sensor", + "isFromConfig": true + }, + "value": 60 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 52, + "propertyName": "Lower limit value of humidity sensor", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 50, + "format": 0, + "allowManualEntry": true, + "label": "Lower limit value of humidity sensor", + "description": "Lower limit value of humidity sensor", + "isFromConfig": true + }, + "value": 50 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 53, + "propertyName": "Upper limit value of Lighting sensor", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 0, + "max": 30000, + "default": 1000, + "format": 0, + "allowManualEntry": true, + "label": "Upper limit value of Lighting sensor", + "description": "Upper limit value of Lighting sensor", + "isFromConfig": true + }, + "value": 1000 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 54, + "propertyName": "Lower limit value of Lighting sensor", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 0, + "max": 30000, + "default": 100, + "format": 0, + "allowManualEntry": true, + "label": "Lower limit value of Lighting sensor", + "description": "Lower limit value of Lighting sensor", + "isFromConfig": true + }, + "value": 100 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 55, + "propertyName": "Upper limit value of ultraviolet sensor", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 11, + "default": 8, + "format": 0, + "allowManualEntry": true, + "label": "Upper limit value of ultraviolet sensor", + "description": "Upper limit value of ultraviolet sensor", + "isFromConfig": true + }, + "value": 8 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 56, + "propertyName": "Lower limit value of ultraviolet sensor", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 11, + "default": 4, + "format": 0, + "allowManualEntry": true, + "label": "Lower limit value of ultraviolet sensor", + "description": "Lower limit value of ultraviolet sensor", + "isFromConfig": true + }, + "value": 4 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 57, + "propertyName": "Recover limit value of temperature sensor", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 0, + "max": 65535, + "default": 0, + "format": 1, + "allowManualEntry": true, + "label": "Recover limit value of temperature sensor", + "description": "Recover limit value of temperature sensor", + "isFromConfig": true + }, + "value": 5122 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 58, + "propertyName": "Recover limit value of humidity sensor", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 50, + "default": 5, + "format": 0, + "allowManualEntry": true, + "label": "Recover limit value of humidity sensor", + "description": "Recover limit value of humidity sensor", + "isFromConfig": true + }, + "value": 5 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 59, + "propertyName": "Recover limit value of Lighting sensor.", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 255, + "default": 10, + "format": 1, + "allowManualEntry": true, + "label": "Recover limit value of Lighting sensor.", + "description": "Recover limit value of Lighting sensor.", + "isFromConfig": true + }, + "value": 10 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 60, + "propertyName": "Recover limit value of Ultraviolet sensor", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 5, + "default": 2, + "format": 0, + "allowManualEntry": true, + "label": "Recover limit value of Ultraviolet sensor", + "description": "Recover limit value of Ultraviolet sensor", + "isFromConfig": true + }, + "value": 2 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 61, + "propertyName": "Out-of-limit state of the Sensors", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": true, + "label": "Out-of-limit state of the Sensors", + "description": "Out-of-limit state of the Sensors", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 64, + "propertyName": "Default unit of the automatic temperature report", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 2, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Default unit of the automatic temperature report", + "description": "Default unit of the automatic temperature report", + "isFromConfig": true + }, + "value": 2 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 81, + "propertyName": "LED function", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 2, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Enable LED blinking", + "1": "Disable PIR LED", + "2": "Disable ALL" + }, + "label": "LED function", + "description": "Disable/Enable LED function", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 111, + "propertyName": "Group 1 Report Interval", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 5, + "max": 2678400, + "default": 3600, + "format": 0, + "allowManualEntry": true, + "label": "Group 1 Report Interval", + "description": "How often to update Group 1", + "isFromConfig": true + }, + "value": 3600 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 112, + "propertyName": "Group 2 Report Interval", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 5, + "max": 2678400, + "default": 3600, + "format": 0, + "allowManualEntry": true, + "label": "Group 2 Report Interval", + "description": "Group 2 Report Interval", + "isFromConfig": true + }, + "value": 3600 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 113, + "propertyName": "Group 3 Report Interval", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 5, + "max": 2678400, + "default": 3600, + "format": 0, + "allowManualEntry": true, + "label": "Group 3 Report Interval", + "description": "Group 3 Report Interval", + "isFromConfig": true + }, + "value": 3600 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 202, + "propertyName": "Humidity Sensor Calibration", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": -50, + "max": 50, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Humidity Sensor Calibration", + "description": "Humidity Sensor Calibration", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 203, + "propertyName": "Luminance Sensor Calibration", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": -1000, + "max": 1000, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Luminance Sensor Calibration", + "description": "Luminance Sensor Calibration", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 204, + "propertyName": "Ultraviolet Sensor Calibration", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": -10, + "max": 10, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Ultraviolet Sensor Calibration", + "description": "Ultraviolet Sensor Calibration", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 252, + "propertyName": "Disable/Enable Configuration Lock", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Disable", + "1": "Enable" + }, + "label": "Disable/Enable Configuration Lock", + "description": "Disable/Enable Configuration Lock (0=Disable, 1=Enable)", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 101, + "propertyKey": 1, + "propertyName": "Group 1: Send battery reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 1: Send battery reports", + "description": "Include battery information in periodic reports to Group 1", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 101, + "propertyKey": 16, + "propertyName": "Group 1: Send ultraviolet reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 1: Send ultraviolet reports", + "description": "Include ultraviolet information in periodic reports to Group 1", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 101, + "propertyKey": 32, + "propertyName": "Group 1: Send temperature reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 1: Send temperature reports", + "description": "Include temperature information in periodic reports to Group 1", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 101, + "propertyKey": 64, + "propertyName": "Group 1: Send humidity reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 1: Send humidity reports", + "description": "Include humidity information in periodic reports to Group 1", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 101, + "propertyKey": 128, + "propertyName": "Group 1: Send luminance reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 1: Send luminance reports", + "description": "Include luminance information in periodic reports to Group 1", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 102, + "propertyKey": 1, + "propertyName": "Group 2: Send battery reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 2: Send battery reports", + "description": "Include battery information in periodic reports to Group 2", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 102, + "propertyKey": 16, + "propertyName": "Group 2: Send ultraviolet reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 2: Send ultraviolet reports", + "description": "Include ultraviolet information in periodic reports to Group 2", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 102, + "propertyKey": 32, + "propertyName": "Group 2: Send temperature reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 2: Send temperature reports", + "description": "Include temperature information in periodic reports to Group 2", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 102, + "propertyKey": 64, + "propertyName": "Group 2: Send humidity reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 2: Send humidity reports", + "description": "Include humidity information in periodic reports to Group 2", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 102, + "propertyKey": 128, + "propertyName": "Group 2: Send luminance reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 2: Send luminance reports", + "description": "Include luminance information in periodic reports to Group 2", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 103, + "propertyKey": 1, + "propertyName": "Group 3: Send battery reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 3: Send battery reports", + "description": "Include battery information in periodic reports to Group 3", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 103, + "propertyKey": 16, + "propertyName": "Group 3: Send ultraviolet reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 3: Send ultraviolet reports", + "description": "Include ultraviolet information in periodic reports to Group 3", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 103, + "propertyKey": 32, + "propertyName": "Group 3: Send temperature reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 3: Send temperature reports", + "description": "Include temperature information in periodic reports to Group 3", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 103, + "propertyKey": 64, + "propertyName": "Group 3: Send humidity reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 3: Send humidity reports", + "description": "Include humidity information in periodic reports to Group 3", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 103, + "propertyKey": 128, + "propertyName": "Group 3: Send luminance reports", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Group 3: Send luminance reports", + "description": "Include luminance information in periodic reports to Group 3", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 9, + "propertyKey": 1, + "propertyName": "Sleep State", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "valueSize": 2, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Asleep", + "1": "Awake" + }, + "label": "Sleep State", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 9, + "propertyKey": 256, + "propertyName": "Power Mode", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "valueSize": 2, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "USB", + "1": "Battery" + }, + "label": "Power Mode", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 41, + "propertyKey": 15, + "propertyName": "Temperature Threshold (Unit)", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 3, + "min": 1, + "max": 2, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "1": "Celsius", + "2": "Fahrenheit" + }, + "label": "Temperature Threshold (Unit)", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 41, + "propertyKey": 16776960, + "propertyName": "Temperature Threshold", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 3, + "min": 0, + "max": 100, + "default": 20, + "format": 0, + "allowManualEntry": true, + "label": "Temperature Threshold", + "description": "Threshold change in temperature to induce an automatic report.", + "isFromConfig": true + }, + "value": 5122 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 49, + "propertyKey": 65280, + "propertyName": "Upper temperature limit (Unit)", + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "valueSize": 4, + "min": 1, + "max": 2, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "1": "Celsius", + "2": "Fahrenheit" + }, + "label": "Upper temperature limit (Unit)", + "isFromConfig": true + }, + "value": 2 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 49, + "propertyKey": 4294901760, + "propertyName": "Upper temperature limit", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": -400, + "max": 2120, + "default": 280, + "format": 0, + "allowManualEntry": true, + "label": "Upper temperature limit", + "isFromConfig": true + }, + "value": 824 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 50, + "propertyKey": 65280, + "propertyName": "Lower temperature limit (Unit)", + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "valueSize": 4, + "min": 1, + "max": 2, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "1": "Celsius", + "2": "Fahrenheit" + }, + "label": "Lower temperature limit (Unit)", + "isFromConfig": true + }, + "value": 2 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 50, + "propertyKey": 4294901760, + "propertyName": "Lower temperature limit", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": -400, + "max": 2120, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Lower temperature limit", + "isFromConfig": true + }, + "value": 320 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 201, + "propertyKey": 255, + "propertyName": "Temperature Calibration (Unit)", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 1, + "max": 2, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "1": "Celsius", + "2": "Fahrenheit" + }, + "label": "Temperature Calibration (Unit)", + "isFromConfig": true + }, + "value": 2 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 201, + "propertyKey": 65280, + "propertyName": "Temperature Calibration", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": -127, + "max": 127, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Temperature Calibration", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 100, + "propertyName": "Set parameters 101-103 to default.", + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Set parameters 101-103 to default.", + "description": "Reset 101-103 to defaults", + "isFromConfig": true + } + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 110, + "propertyName": "Set parameters 111-113 to default.", + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Set parameters 111-113 to default.", + "description": "Set parameters 111-113 to default.", + "isFromConfig": true + } + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 255, + "propertyName": "Reset to default factory settings", + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1431655765, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "1": "Resets all configuration parameters to defaults", + "1431655765": "Reset to default factory settings and be excluded" + }, + "label": "Reset to default factory settings", + "isFromConfig": true + } + }, + { + "commandClassName": "Notification", + "commandClass": 113, + "endpoint": 0, + "property": "Home Security", + "propertyKey": "Cover status", + "propertyName": "Home Security", + "propertyKeyName": "Cover status", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Cover status", + "states": { + "0": "idle", + "3": "Tampering, product cover removed" + }, + "ccSpecific": { + "notificationType": 7 + } + }, + "value": 0 + }, + { + "commandClassName": "Notification", + "commandClass": 113, + "endpoint": 0, + "property": "Home Security", + "propertyKey": "Motion sensor status", + "propertyName": "Home Security", + "propertyKeyName": "Motion sensor status", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Motion sensor status", + "states": { + "0": "idle", + "8": "Motion detection" + }, + "ccSpecific": { + "notificationType": 7 + } + }, + "value": 8 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "manufacturerId", + "propertyName": "manufacturerId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 134 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productType", + "propertyName": "productType", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 258 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productId", + "propertyName": "productId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 100 + }, + { + "commandClassName": "Battery", + "commandClass": 128, + "endpoint": 0, + "property": "level", + "propertyName": "level", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 100, + "unit": "%", + "label": "Battery level" + }, + "value": 100 + }, + { + "commandClassName": "Battery", + "commandClass": 128, + "endpoint": 0, + "property": "isLow", + "propertyName": "isLow", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false + }, + { + "commandClassName": "Wake Up", + "commandClass": 132, + "endpoint": 0, + "property": "wakeUpInterval", + "propertyName": "wakeUpInterval", + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "min": 240, + "max": 3600, + "label": "Wake Up interval", + "steps": 60, + "default": 3600 + }, + "value": 3600 + }, + { + "commandClassName": "Wake Up", + "commandClass": 132, + "endpoint": 0, + "property": "controllerNodeId", + "propertyName": "controllerNodeId", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Node ID of the controller" + }, + "value": 1 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "libraryType", + "propertyName": "libraryType", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Libary type" + }, + "value": 3 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "protocolVersion", + "propertyName": "protocolVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.54" + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": [ + "1.12" + ] + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + } + ] +} diff --git a/tests/fixtures/zwave_js/nortek_thermostat_added_event.json b/tests/fixtures/zwave_js/nortek_thermostat_added_event.json new file mode 100644 index 00000000000..d778f77ce24 --- /dev/null +++ b/tests/fixtures/zwave_js/nortek_thermostat_added_event.json @@ -0,0 +1,260 @@ +{ + "source": "controller", + "event": "node added", + "node": { + "nodeId": 53, + "index": 0, + "status": 0, + "ready": false, + "deviceClass": { + "basic": "Static Controller", + "generic": "Thermostat", + "specific": "Thermostat General V2", + "mandatorySupportedCCs": [ + "Basic", + "Manufacturer Specific", + "Thermostat Mode", + "Thermostat Setpoint", + "Version" + ], + "mandatoryControlCCs": [] + }, + "neighbors": [], + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 53, + "index": 0 + } + ], + "values": [ + { + "commandClassName": "Basic", + "commandClass": 32, + "endpoint": 0, + "property": "currentValue", + "propertyName": "currentValue", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 99, + "label": "Current value" + } + }, + { + "commandClassName": "Basic", + "commandClass": 32, + "endpoint": 0, + "property": "targetValue", + "propertyName": "targetValue", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 99, + "label": "Target value" + } + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "manufacturerId", + "propertyName": "manufacturerId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + } + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productType", + "propertyName": "productType", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + } + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productId", + "propertyName": "productId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + } + }, + { + "commandClassName": "Thermostat Mode", + "commandClass": 64, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 31, + "label": "Thermostat mode" + } + }, + { + "commandClassName": "Thermostat Mode", + "commandClass": 64, + "endpoint": 0, + "property": "manufacturerData", + "propertyName": "manufacturerData", + "metadata": { + "type": "any", + "readable": true, + "writeable": true + } + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "libraryType", + "propertyName": "libraryType", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + } + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "protocolVersion", + "propertyName": "protocolVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + } + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + } + }, + { + "commandClassName": "Thermostat Operating State", + "commandClass": 66, + "endpoint": 0, + "property": "state", + "propertyName": "state", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Operating state", + "states": { + "0": "Idle", + "1": "Heating", + "2": "Cooling", + "3": "Fan Only", + "4": "Pending Heat", + "5": "Pending Cool", + "6": "Vent/Economizer", + "7": "Aux Heating", + "8": "2nd Stage Heating", + "9": "2nd Stage Cooling", + "10": "2nd Stage Aux Heat", + "11": "3rd Stage Aux Heat" + } + } + }, + { + "commandClassName": "Battery", + "commandClass": 128, + "endpoint": 0, + "property": "level", + "propertyName": "level", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 100, + "unit": "%", + "label": "Battery level" + } + }, + { + "commandClassName": "Battery", + "commandClass": 128, + "endpoint": 0, + "property": "isLow", + "propertyName": "isLow", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + } + }, + { + "commandClassName": "Scene Activation", + "commandClass": 43, + "endpoint": 0, + "property": "sceneId", + "propertyName": "sceneId", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 1, + "max": 255, + "label": "Scene ID" + } + }, + { + "commandClassName": "Scene Activation", + "commandClass": 43, + "endpoint": 0, + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Dimming duration" + } + } + ] + } +} \ No newline at end of file diff --git a/tests/fixtures/zwave_js/nortek_thermostat_removed_event.json b/tests/fixtures/zwave_js/nortek_thermostat_removed_event.json new file mode 100644 index 00000000000..ed25a650543 --- /dev/null +++ b/tests/fixtures/zwave_js/nortek_thermostat_removed_event.json @@ -0,0 +1,283 @@ +{ + "source": "controller", + "event": "node removed", + "node": { + "nodeId": 67, + "index": 0, + "status": 4, + "ready": true, + "deviceClass": { + "basic": "Static Controller", + "generic": "Thermostat", + "specific": "Thermostat General V2", + "mandatorySupportedCCs": [ + "Basic", + "Manufacturer Specific", + "Thermostat Mode", + "Thermostat Setpoint", + "Version" + ], + "mandatoryControlCCs": [] + }, + "isListening": false, + "isFrequentListening": true, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": false, + "version": 4, + "isBeaming": true, + "deviceConfig": { + "manufacturerId": 335, + "manufacturer": "Nortek Security & Control LLC", + "label": "GoControl GC-TBZ48", + "description": "Z-Wave Plus Thermostat", + "devices": [ + { + "productType": "0x5442", + "productId": "0x5431" + }, + { + "productType": "0x5442", + "productId": "0x5436" + }, + { + "productType": "0x5442", + "productId": "0x5437" + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + } + }, + "label": "GoControl GC-TBZ48", + "neighbors": [ + 1, + 32, + 39, + 52 + ], + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 67, + "index": 0 + } + ], + "values": [ + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "manufacturerId", + "propertyName": "manufacturerId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + } + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productType", + "propertyName": "productType", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + } + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productId", + "propertyName": "productId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + } + }, + { + "commandClassName": "Thermostat Mode", + "commandClass": 64, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 31, + "label": "Thermostat mode" + } + }, + { + "commandClassName": "Thermostat Mode", + "commandClass": 64, + "endpoint": 0, + "property": "manufacturerData", + "propertyName": "manufacturerData", + "metadata": { + "type": "any", + "readable": true, + "writeable": true + } + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "libraryType", + "propertyName": "libraryType", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + } + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "protocolVersion", + "propertyName": "protocolVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + } + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + } + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + }, + { + "commandClassName": "Thermostat Operating State", + "commandClass": 66, + "endpoint": 0, + "property": "state", + "propertyName": "state", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Operating state", + "states": { + "0": "Idle", + "1": "Heating", + "2": "Cooling", + "3": "Fan Only", + "4": "Pending Heat", + "5": "Pending Cool", + "6": "Vent/Economizer", + "7": "Aux Heating", + "8": "2nd Stage Heating", + "9": "2nd Stage Cooling", + "10": "2nd Stage Aux Heat", + "11": "3rd Stage Aux Heat" + } + } + }, + { + "commandClassName": "Battery", + "commandClass": 128, + "endpoint": 0, + "property": "level", + "propertyName": "level", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 100, + "unit": "%", + "label": "Battery level" + } + }, + { + "commandClassName": "Battery", + "commandClass": 128, + "endpoint": 0, + "property": "isLow", + "propertyName": "isLow", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + } + }, + { + "commandClassName": "Scene Activation", + "commandClass": 43, + "endpoint": 0, + "property": "sceneId", + "propertyName": "sceneId", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 1, + "max": 255, + "label": "Scene ID" + } + }, + { + "commandClassName": "Scene Activation", + "commandClass": 43, + "endpoint": 0, + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Dimming duration" + } + } + ] + } +} \ No newline at end of file diff --git a/tests/fixtures/zwave_js/nortek_thermostat_state.json b/tests/fixtures/zwave_js/nortek_thermostat_state.json new file mode 100644 index 00000000000..62a08999cda --- /dev/null +++ b/tests/fixtures/zwave_js/nortek_thermostat_state.json @@ -0,0 +1,1284 @@ +{ + "nodeId": 67, + "index": 0, + "installerIcon": 4608, + "userIcon": 4608, + "status": 4, + "ready": true, + "deviceClass": { + "basic": "Static Controller", + "generic": "Thermostat", + "specific": "Thermostat General V2", + "mandatorySupportedCCs": [ + "Basic", + "Manufacturer Specific", + "Thermostat Mode", + "Thermostat Setpoint", + "Version" + ], + "mandatoryControlCCs": [] + }, + "isListening": false, + "isFrequentListening": true, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": false, + "version": 4, + "isBeaming": true, + "manufacturerId": 335, + "productId": 21559, + "productType": 21570, + "firmwareVersion": "1.0", + "zwavePlusVersion": 1, + "nodeType": 0, + "roleType": 7, + "deviceConfig": { + "manufacturerId": 335, + "manufacturer": "Nortek Security & Control LLC", + "label": "GoControl GC-TBZ48", + "description": "Z-Wave Plus Thermostat", + "devices": [ + { + "productType": "0x5442", + "productId": "0x5431" + }, + { + "productType": "0x5442", + "productId": "0x5436" + }, + { + "productType": "0x5442", + "productId": "0x5437" + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + } + }, + "label": "GoControl GC-TBZ48", + "neighbors": [ + 1, + 32, + 39, + 52 + ], + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 67, + "index": 0, + "installerIcon": 4608, + "userIcon": 4608 + } + ], + "values": [ + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "manufacturerId", + "propertyName": "manufacturerId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 335 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productType", + "propertyName": "productType", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 21570 + }, + { + "commandClassName": "Manufacturer Specific", + "commandClass": 114, + "endpoint": 0, + "property": "productId", + "propertyName": "productId", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 21559 + }, + { + "commandClassName": "Thermostat Mode", + "commandClass": 64, + "endpoint": 0, + "property": "mode", + "propertyName": "mode", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 31, + "label": "Thermostat mode", + "states": { + "0": "Off", + "1": "Heat", + "2": "Cool", + "3": "Auto", + "4": "Auxiliary" + } + }, + "value": 0 + }, + { + "commandClassName": "Thermostat Mode", + "commandClass": 64, + "endpoint": 0, + "property": "manufacturerData", + "propertyName": "manufacturerData", + "metadata": { + "type": "any", + "readable": true, + "writeable": true + } + }, + { + "commandClassName": "Thermostat Setpoint", + "commandClass": 67, + "endpoint": 0, + "property": "setpoint", + "propertyKey": 1, + "propertyName": "setpoint", + "propertyKeyName": "Heating", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "unit": "°F", + "ccSpecific": { + "setpointType": 1 + } + }, + "value": 72 + }, + { + "commandClassName": "Thermostat Setpoint", + "commandClass": 67, + "endpoint": 0, + "property": "setpoint", + "propertyKey": 2, + "propertyName": "setpoint", + "propertyKeyName": "Cooling", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "unit": "°F", + "ccSpecific": { + "setpointType": 2 + } + }, + "value": 80 + }, + { + "commandClassName": "Thermostat Setpoint", + "commandClass": 67, + "endpoint": 0, + "property": "setpoint", + "propertyKey": 11, + "propertyName": "setpoint", + "propertyKeyName": "Energy Save Heating", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "unit": "°F", + "ccSpecific": { + "setpointType": 11 + } + }, + "value": 62 + }, + { + "commandClassName": "Thermostat Setpoint", + "commandClass": 67, + "endpoint": 0, + "property": "setpoint", + "propertyKey": 12, + "propertyName": "setpoint", + "propertyKeyName": "Energy Save Cooling", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "unit": "°F", + "ccSpecific": { + "setpointType": 12 + } + }, + "value": 80 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "libraryType", + "propertyName": "libraryType", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 6 + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "protocolVersion", + "propertyName": "protocolVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.5" + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": [ + "1.0" + ] + }, + { + "commandClassName": "Version", + "commandClass": 134, + "endpoint": 0, + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + }, + { + "commandClassName": "Thermostat Operating State", + "commandClass": 66, + "endpoint": 0, + "property": "state", + "propertyName": "state", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Operating state", + "states": { + "0": "Idle", + "1": "Heating", + "2": "Cooling", + "3": "Fan Only", + "4": "Pending Heat", + "5": "Pending Cool", + "6": "Vent/Economizer", + "7": "Aux Heating", + "8": "2nd Stage Heating", + "9": "2nd Stage Cooling", + "10": "2nd Stage Aux Heat", + "11": "3rd Stage Aux Heat" + } + }, + "value": 64 + }, + { + "commandClassName": "Battery", + "commandClass": 128, + "endpoint": 0, + "property": "level", + "propertyName": "level", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 100, + "unit": "%", + "label": "Battery level" + }, + "value": 100 + }, + { + "commandClassName": "Battery", + "commandClass": 128, + "endpoint": 0, + "property": "isLow", + "propertyName": "isLow", + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 1, + "propertyName": "System Type", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Standard", + "1": "Heat Pump" + }, + "label": "System Type", + "description": "System Type", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 2, + "propertyName": "Fan Type", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Gas", + "1": "Electric" + }, + "label": "Fan Type", + "description": "Fan Type", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 3, + "propertyName": "Change Over Type", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "w/Cool", + "1": "w/Heat" + }, + "label": "Change Over Type", + "description": "Change Over Type", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 4, + "propertyName": "2nd Stage Heat Enable", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "label": "2nd Stage Heat Enable", + "description": "2nd Stage Heat Enable", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 5, + "propertyName": "Aux Heat Enable", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "label": "Aux Heat Enable", + "description": "Aux Heat Enable", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 6, + "propertyName": "2nd Stage Cool Enable", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "label": "2nd Stage Cool Enable", + "description": "2nd Stage Cool Enable", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 7, + "propertyName": "Temperature Unit", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Celsius", + "1": "Fahrenheit" + }, + "label": "Temperature Unit", + "description": "Temperature Unit", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 8, + "propertyName": "Minimum Off Time", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 5, + "max": 9, + "default": 5, + "format": 0, + "allowManualEntry": true, + "label": "Minimum Off Time", + "description": "Minimum Off Time", + "isFromConfig": true + }, + "value": 5 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 9, + "propertyName": "Minimum Run Time", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 3, + "max": 9, + "default": 3, + "format": 0, + "allowManualEntry": true, + "label": "Minimum Run Time", + "description": "Minimum Run Time", + "isFromConfig": true + }, + "value": 3 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 10, + "propertyName": "Setpoint H/C Delta", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 3, + "max": 15, + "default": 3, + "format": 0, + "allowManualEntry": true, + "label": "Setpoint H/C Delta", + "description": "Setpoint H/C Delta", + "isFromConfig": true + }, + "value": 3 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 11, + "propertyName": "H Delta Stage 1 ON", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 6, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "H Delta Stage 1 ON", + "description": "H Delta Stage 1 ON", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 12, + "propertyName": "H Delta Stage 1 OFF", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 5, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "H Delta Stage 1 OFF", + "description": "H Delta Stage 1 OFF", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 13, + "propertyName": "H Delta Stage 2 ON", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 2, + "max": 7, + "default": 2, + "format": 0, + "allowManualEntry": true, + "label": "H Delta Stage 2 ON", + "description": "H Delta Stage 2 ON", + "isFromConfig": true + }, + "value": 2 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 14, + "propertyName": "H Delta Stage 2 OFF", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 6, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "H Delta Stage 2 OFF", + "description": "H Delta Stage 2 OFF", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 15, + "propertyName": "H Delta Aux ON", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 3, + "max": 8, + "default": 3, + "format": 0, + "allowManualEntry": true, + "label": "H Delta Aux ON", + "description": "H Delta Aux ON", + "isFromConfig": true + }, + "value": 3 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 16, + "propertyName": "H Delta Stage 3 OFF", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 7, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "H Delta Stage 3 OFF", + "description": "H Delta Stage 3 OFF", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 17, + "propertyName": "C Delta Stage 1 ON", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 6, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "C Delta Stage 1 ON", + "description": "C Delta Stage 1 ON", + "isFromConfig": true + }, + "value": 1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 18, + "propertyName": "C Delta Stage 1 OFF", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 5, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "C Delta Stage 1 OFF", + "description": "C Delta Stage 1 OFF", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 19, + "propertyName": "C Delta Stage 2 ON", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 2, + "max": 7, + "default": 2, + "format": 0, + "allowManualEntry": true, + "label": "C Delta Stage 2 ON", + "description": "C Delta Stage 2 ON", + "isFromConfig": true + }, + "value": 2 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 20, + "propertyName": "C Delta Stage 2 OFF", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 6, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "C Delta Stage 2 OFF", + "description": "C Delta Stage 2 OFF", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 23, + "propertyName": "Lifeline Association Group Report To Send", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 65535, + "default": 8319, + "format": 0, + "allowManualEntry": true, + "label": "Lifeline Association Group Report To Send", + "description": "Lifeline Association Group Report To Send", + "isFromConfig": true + }, + "value": 8287 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 24, + "propertyName": "Display Lock", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Unlocked", + "1": "Locked" + }, + "label": "Display Lock", + "description": "Display Lock", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 26, + "propertyName": "Backlight Timer", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 10, + "max": 30, + "default": 20, + "format": 0, + "allowManualEntry": true, + "label": "Backlight Timer", + "description": "Backlight Timer", + "isFromConfig": true + }, + "value": 20 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 33, + "propertyName": "Max Heat Setpoint", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 30, + "max": 109, + "default": 90, + "format": 0, + "allowManualEntry": true, + "label": "Max Heat Setpoint", + "description": "Max Heat Setpoint", + "isFromConfig": true + }, + "value": 90 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 34, + "propertyName": "Min Cool Setpoint", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 33, + "max": 112, + "default": 61, + "format": 0, + "allowManualEntry": true, + "label": "Min Cool Setpoint", + "description": "Min Cool Setpoint", + "isFromConfig": true + }, + "value": 60 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 38, + "propertyName": "Schedule Enable", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Disabled", + "1": "Enabled" + }, + "label": "Schedule Enable", + "description": "Schedule Enable", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 39, + "propertyName": "Run/Hold Mode", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Hold", + "1": "Run" + }, + "label": "Run/Hold Mode", + "description": "Run/Hold Mode", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 40, + "propertyName": "Setback Mode", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 2, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "No Setback", + "2": "Unoccupied Mode" + }, + "label": "Setback Mode", + "description": "Setback Mode", + "isFromConfig": true + }, + "value": -1 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 41, + "propertyName": "Un-Occupied HSP", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 30, + "max": 109, + "default": 62, + "format": 0, + "allowManualEntry": true, + "label": "Un-Occupied HSP", + "description": "Un-Occupied HSP", + "isFromConfig": true + }, + "value": 62 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 42, + "propertyName": "Un-Occupied CSP", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 33, + "max": 112, + "default": 80, + "format": 0, + "allowManualEntry": true, + "label": "Un-Occupied CSP", + "description": "Un-Occupied CSP", + "isFromConfig": true + }, + "value": 80 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 43, + "propertyName": "Remote Sensor 1 Node Number", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 252, + "default": 0, + "format": 1, + "allowManualEntry": true, + "label": "Remote Sensor 1 Node Number", + "description": "Remote Sensor 1 Node Number", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 46, + "propertyName": "Remote Sensor 1 Temperature", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 112, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Remote Sensor 1 Temperature", + "description": "Remote Sensor 1 Temperature", + "isFromConfig": true + }, + "value": 71 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 48, + "propertyName": "Internal Sensor Temp Offset", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": -7, + "max": 7, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Internal Sensor Temp Offset", + "description": "Internal Sensor Temp Offset", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 49, + "propertyName": "R1 Sensor Temp Offset", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": -7, + "max": 7, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "R1 Sensor Temp Offset", + "description": "R1 Sensor Temp Offset", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 54, + "propertyName": "Heat Timer", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 0, + "max": 4000, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Heat Timer", + "description": "Heat Timer", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 55, + "propertyName": "Cool Timer", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 0, + "max": 4000, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Cool Timer", + "description": "Cool Timer", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 61, + "propertyName": "Fan Purge Heat", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 90, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Fan Purge Heat", + "description": "Fan Purge Heat", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 62, + "propertyName": "Fan Purge Cool", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 90, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Fan Purge Cool", + "description": "Fan Purge Cool", + "isFromConfig": true + }, + "value": 0 + }, + { + "commandClassName": "Multilevel Sensor", + "commandClass": 49, + "endpoint": 0, + "property": "Air temperature", + "propertyName": "Air temperature", + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "unit": "°F", + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 1 + } + }, + "value": 71 + }, + { + "commandClassName": "Scene Activation", + "commandClass": 43, + "endpoint": 0, + "property": "sceneId", + "propertyName": "sceneId", + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 1, + "max": 255, + "label": "Scene ID" + } + }, + { + "commandClassName": "Scene Activation", + "commandClass": 43, + "endpoint": 0, + "property": "dimmingDuration", + "propertyName": "dimmingDuration", + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Dimming duration" + } + } + ] +} \ No newline at end of file diff --git a/tests/helpers/test_aiohttp_client.py b/tests/helpers/test_aiohttp_client.py index 0f37ebd7b3b..e6f113c7699 100644 --- a/tests/helpers/test_aiohttp_client.py +++ b/tests/helpers/test_aiohttp_client.py @@ -1,5 +1,6 @@ """Test the aiohttp client helper.""" import asyncio +from unittest.mock import Mock, patch import aiohttp import pytest @@ -8,8 +9,6 @@ from homeassistant.core import EVENT_HOMEASSISTANT_CLOSE import homeassistant.helpers.aiohttp_client as client from homeassistant.setup import async_setup_component -from tests.async_mock import Mock, patch - @pytest.fixture(name="camera_client") def camera_client_fixture(hass, hass_client): diff --git a/tests/helpers/test_area_registry.py b/tests/helpers/test_area_registry.py index 4b6ca7da3fe..ec008dde7da 100644 --- a/tests/helpers/test_area_registry.py +++ b/tests/helpers/test_area_registry.py @@ -1,12 +1,12 @@ """Tests for the Area Registry.""" import asyncio +import unittest.mock import pytest from homeassistant.core import callback from homeassistant.helpers import area_registry -import tests.async_mock from tests.common import flush_store, mock_area_registry @@ -178,7 +178,7 @@ async def test_loading_area_from_storage(hass, hass_storage): async def test_loading_race_condition(hass): """Test only one storage load called when concurrent loading occurred .""" - with tests.async_mock.patch( + with unittest.mock.patch( "homeassistant.helpers.area_registry.AreaRegistry.async_load" ) as mock_load: results = await asyncio.gather( diff --git a/tests/helpers/test_check_config.py b/tests/helpers/test_check_config.py index 7959cf66403..c5b75b84342 100644 --- a/tests/helpers/test_check_config.py +++ b/tests/helpers/test_check_config.py @@ -1,5 +1,6 @@ """Test check_config helper.""" import logging +from unittest.mock import Mock, patch from homeassistant.config import YAML_CONFIG_FILE from homeassistant.helpers.check_config import ( @@ -7,7 +8,6 @@ from homeassistant.helpers.check_config import ( async_check_ha_config_file, ) -from tests.async_mock import Mock, patch from tests.common import mock_platform, patch_yaml_files _LOGGER = logging.getLogger(__name__) diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 802ad7699e3..fe2a9aa4406 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -1,5 +1,6 @@ """Test the condition helper.""" from logging import ERROR +from unittest.mock import patch import pytest @@ -9,8 +10,6 @@ from homeassistant.helpers.template import Template from homeassistant.setup import async_setup_component from homeassistant.util import dt -from tests.async_mock import patch - async def test_invalid_condition(hass): """Test if invalid condition raises.""" diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index 926aa98e308..874fd5df29a 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -1,11 +1,12 @@ """Tests for the Config Entry Flow helper.""" +from unittest.mock import Mock, patch + import pytest from homeassistant import config_entries, data_entry_flow, setup from homeassistant.config import async_process_ha_core_config from homeassistant.helpers import config_entry_flow -from tests.async_mock import Mock, patch from tests.common import ( MockConfigEntry, MockModule, @@ -81,7 +82,7 @@ async def test_user_has_confirmation(hass, discovery_flow_conf): assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY -@pytest.mark.parametrize("source", ["discovery", "mqtt", "ssdp", "zeroconf"]) +@pytest.mark.parametrize("source", ["discovery", "mqtt", "ssdp", "zeroconf", "dhcp"]) async def test_discovery_single_instance(hass, discovery_flow_conf, source): """Test we not allow duplicates.""" flow = config_entries.HANDLERS["test"]() @@ -95,7 +96,7 @@ async def test_discovery_single_instance(hass, discovery_flow_conf, source): assert result["reason"] == "single_instance_allowed" -@pytest.mark.parametrize("source", ["discovery", "mqtt", "ssdp", "zeroconf"]) +@pytest.mark.parametrize("source", ["discovery", "mqtt", "ssdp", "zeroconf", "dhcp"]) async def test_discovery_confirmation(hass, discovery_flow_conf, source): """Test we ask for confirmation via discovery.""" flow = config_entries.HANDLERS["test"]() @@ -219,7 +220,7 @@ async def test_ignored_discoveries(hass, discovery_flow_conf): await hass.config_entries.flow.async_init( flow["handler"], context={"source": config_entries.SOURCE_IGNORE}, - data={"unique_id": flow["context"]["unique_id"]}, + data={"unique_id": flow["context"]["unique_id"], "title": "Ignored Entry"}, ) # Second discovery should be aborted diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index f2f2db37d7f..617c4690696 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -2,6 +2,7 @@ import asyncio import logging import time +from unittest.mock import patch import aiohttp import pytest @@ -10,7 +11,6 @@ from homeassistant import config_entries, data_entry_flow, setup from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.network import NoURLAvailableError -from tests.async_mock import patch from tests.common import MockConfigEntry, mock_platform TEST_DOMAIN = "oauth2_test" diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 5d907408b61..1397e499c7e 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -3,6 +3,7 @@ from datetime import date, datetime, timedelta import enum import os from socket import _GLOBAL_DEFAULT_TIMEOUT +from unittest.mock import Mock, patch import uuid import pytest @@ -11,8 +12,6 @@ import voluptuous as vol import homeassistant from homeassistant.helpers import config_validation as cv, template -from tests.async_mock import Mock, patch - def test_boolean(): """Test boolean validation.""" diff --git a/tests/helpers/test_debounce.py b/tests/helpers/test_debounce.py index e9cb5749eaf..cdced565e73 100644 --- a/tests/helpers/test_debounce.py +++ b/tests/helpers/test_debounce.py @@ -1,7 +1,7 @@ """Tests for debounce.""" -from homeassistant.helpers import debounce +from unittest.mock import AsyncMock -from tests.async_mock import AsyncMock +from homeassistant.helpers import debounce async def test_immediate_works(hass): diff --git a/tests/helpers/test_deprecation.py b/tests/helpers/test_deprecation.py index c7e903f7b16..ebabe12c1ad 100644 --- a/tests/helpers/test_deprecation.py +++ b/tests/helpers/test_deprecation.py @@ -1,7 +1,11 @@ """Test deprecation helpers.""" -from homeassistant.helpers.deprecation import deprecated_substitute, get_deprecated +from unittest.mock import MagicMock, patch -from tests.async_mock import MagicMock, patch +from homeassistant.helpers.deprecation import ( + deprecated_function, + deprecated_substitute, + get_deprecated, +) class MockBaseClass: @@ -78,3 +82,17 @@ def test_config_get_deprecated_new(mock_get_logger): config = {"new_name": True} assert get_deprecated(config, "new_name", "old_name") is True assert not mock_logger.warning.called + + +def test_deprecated_function(caplog): + """Test deprecated_function decorator.""" + + @deprecated_function("new_function") + def mock_deprecated_function(): + pass + + mock_deprecated_function() + assert ( + "mock_deprecated_function is a deprecated function. Use new_function instead" + in caplog.text + ) diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 7fa787e023e..01959174335 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -1,5 +1,7 @@ """Tests for the Device Registry.""" import asyncio +import time +from unittest.mock import patch import pytest @@ -7,7 +9,6 @@ from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import CoreState, callback from homeassistant.helpers import device_registry, entity_registry -from tests.async_mock import patch from tests.common import MockConfigEntry, flush_store, mock_device_registry @@ -229,8 +230,8 @@ async def test_removing_config_entries(hass, registry, update_events): assert entry2.config_entries == {"123", "456"} registry.async_clear_config_entry("123") - entry = registry.async_get_device({("bridgeid", "0123")}, set()) - entry3_removed = registry.async_get_device({("bridgeid", "4567")}, set()) + entry = registry.async_get_device({("bridgeid", "0123")}) + entry3_removed = registry.async_get_device({("bridgeid", "4567")}) assert entry.config_entries == {"456"} assert entry3_removed is None @@ -301,26 +302,41 @@ async def test_deleted_device_removing_config_entries(hass, registry, update_eve registry.async_clear_config_entry("123") assert len(registry.devices) == 0 - assert len(registry.deleted_devices) == 1 + assert len(registry.deleted_devices) == 2 registry.async_clear_config_entry("456") assert len(registry.devices) == 0 - assert len(registry.deleted_devices) == 0 + assert len(registry.deleted_devices) == 2 # No event when a deleted device is purged await hass.async_block_till_done() assert len(update_events) == 5 - # Re-add, expect new device id + # Re-add, expect to keep the device id entry2 = registry.async_get_or_create( - config_entry_id="123", + config_entry_id="456", connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, identifiers={("bridgeid", "0123")}, manufacturer="manufacturer", model="model", ) - assert entry.id != entry2.id + assert entry.id == entry2.id + + future_time = time.time() + device_registry.ORPHANED_DEVICE_KEEP_SECONDS + 1 + + with patch("time.time", return_value=future_time): + registry.async_purge_expired_orphaned_devices() + + # Re-add, expect to get a new device id after the purge + entry4 = registry.async_get_or_create( + config_entry_id="123", + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, + identifiers={("bridgeid", "0123")}, + manufacturer="manufacturer", + model="model", + ) + assert entry3.id != entry4.id async def test_removing_area_id(registry): @@ -336,7 +352,7 @@ async def test_removing_area_id(registry): entry_w_area = registry.async_update_device(entry.id, area_id="12345A") registry.async_clear_area_id("12345A") - entry_wo_area = registry.async_get_device({("bridgeid", "0123")}, set()) + entry_wo_area = registry.async_get_device({("bridgeid", "0123")}) assert not entry_wo_area.area_id assert entry_w_area != entry_wo_area @@ -366,7 +382,7 @@ async def test_deleted_device_removing_area_id(registry): ) assert entry.id == entry2.id - entry_wo_area = registry.async_get_device({("bridgeid", "0123")}, set()) + entry_wo_area = registry.async_get_device({("bridgeid", "0123")}) assert not entry_wo_area.area_id assert entry_w_area != entry_wo_area @@ -505,9 +521,9 @@ async def test_loading_saving_data(hass, registry): assert list(registry.devices) == list(registry2.devices) assert list(registry.deleted_devices) == list(registry2.deleted_devices) - new_via = registry2.async_get_device({("hue", "0123")}, set()) - new_light = registry2.async_get_device({("hue", "456")}, set()) - new_light4 = registry2.async_get_device({("hue", "abc")}, set()) + new_via = registry2.async_get_device({("hue", "0123")}) + new_light = registry2.async_get_device({("hue", "456")}) + new_light4 = registry2.async_get_device({("hue", "abc")}) assert orig_via == new_via assert orig_light == new_light @@ -597,11 +613,11 @@ async def test_update(registry): assert updated_entry.via_device_id == "98765B" assert updated_entry.disabled_by == "user" - assert registry.async_get_device({("hue", "456")}, {}) is None - assert registry.async_get_device({("bla", "123")}, {}) is None + assert registry.async_get_device({("hue", "456")}) is None + assert registry.async_get_device({("bla", "123")}) is None - assert registry.async_get_device({("hue", "654")}, {}) == updated_entry - assert registry.async_get_device({("bla", "321")}, {}) == updated_entry + assert registry.async_get_device({("hue", "654")}) == updated_entry + assert registry.async_get_device({("bla", "321")}) == updated_entry assert ( registry.async_get_device( @@ -652,7 +668,7 @@ async def test_update_remove_config_entries(hass, registry, update_events): assert updated_entry.config_entries == {"456"} assert removed_entry is None - removed_entry = registry.async_get_device({("bridgeid", "4567")}, set()) + removed_entry = registry.async_get_device({("bridgeid", "4567")}) assert removed_entry is None @@ -728,10 +744,44 @@ async def test_cleanup_device_registry(hass, registry): device_registry.async_cleanup(hass, registry, ent_reg) - assert registry.async_get_device({("hue", "d1")}, set()) is not None - assert registry.async_get_device({("hue", "d2")}, set()) is not None - assert registry.async_get_device({("hue", "d3")}, set()) is not None - assert registry.async_get_device({("something", "d4")}, set()) is None + assert registry.async_get_device({("hue", "d1")}) is not None + assert registry.async_get_device({("hue", "d2")}) is not None + assert registry.async_get_device({("hue", "d3")}) is not None + assert registry.async_get_device({("something", "d4")}) is None + + +async def test_cleanup_device_registry_removes_expired_orphaned_devices(hass, registry): + """Test cleanup removes expired orphaned devices.""" + config_entry = MockConfigEntry(domain="hue") + config_entry.add_to_hass(hass) + + registry.async_get_or_create( + identifiers={("hue", "d1")}, config_entry_id=config_entry.entry_id + ) + registry.async_get_or_create( + identifiers={("hue", "d2")}, config_entry_id=config_entry.entry_id + ) + registry.async_get_or_create( + identifiers={("hue", "d3")}, config_entry_id=config_entry.entry_id + ) + + registry.async_clear_config_entry(config_entry.entry_id) + assert len(registry.devices) == 0 + assert len(registry.deleted_devices) == 3 + + ent_reg = await entity_registry.async_get_registry(hass) + device_registry.async_cleanup(hass, registry, ent_reg) + + assert len(registry.devices) == 0 + assert len(registry.deleted_devices) == 3 + + future_time = time.time() + device_registry.ORPHANED_DEVICE_KEEP_SECONDS + 1 + + with patch("time.time", return_value=future_time): + device_registry.async_cleanup(hass, registry, ent_reg) + + assert len(registry.devices) == 0 + assert len(registry.deleted_devices) == 0 async def test_cleanup_startup(hass): diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 15e9bf55aa4..52149b060e4 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -3,6 +3,7 @@ import asyncio from datetime import timedelta import threading +from unittest.mock import MagicMock, PropertyMock, patch import pytest @@ -10,7 +11,6 @@ from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNAVAILABLE from homeassistant.core import Context from homeassistant.helpers import entity, entity_registry -from tests.async_mock import MagicMock, PropertyMock, patch from tests.common import ( MockConfigEntry, MockEntity, diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index 0373705186f..8d61ec7d509 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -3,6 +3,7 @@ from collections import OrderedDict from datetime import timedelta import logging +from unittest.mock import AsyncMock, Mock, patch import pytest import voluptuous as vol @@ -15,7 +16,6 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import AsyncMock, Mock, patch from tests.common import ( MockConfigEntry, MockEntity, diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index d12c574d1a9..0a939ba2825 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -2,6 +2,7 @@ import asyncio from datetime import timedelta import logging +from unittest.mock import Mock, patch import pytest @@ -16,7 +17,6 @@ from homeassistant.helpers.entity_component import ( ) import homeassistant.util.dt as dt_util -from tests.async_mock import Mock, patch from tests.common import ( MockConfigEntry, MockEntity, @@ -747,7 +747,7 @@ async def test_device_info_called(hass): assert len(hass.states.async_entity_ids()) == 2 - device = registry.async_get_device({("hue", "1234")}, set()) + device = registry.async_get_device({("hue", "1234")}) assert device is not None assert device.identifiers == {("hue", "1234")} assert device.connections == {("mac", "abcd")} diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index 19af3715160..21f4392122e 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -1,5 +1,7 @@ """Tests for the Entity Registry.""" import asyncio +import unittest.mock +from unittest.mock import patch import pytest @@ -7,8 +9,6 @@ from homeassistant.const import EVENT_HOMEASSISTANT_START, STATE_UNAVAILABLE from homeassistant.core import CoreState, callback, valid_entity_id from homeassistant.helpers import entity_registry -import tests.async_mock -from tests.async_mock import patch from tests.common import ( MockConfigEntry, flush_store, @@ -429,7 +429,7 @@ async def test_loading_invalid_entity_id(hass, hass_storage): async def test_loading_race_condition(hass): """Test only one storage load called when concurrent loading occurred .""" - with tests.async_mock.patch( + with unittest.mock.patch( "homeassistant.helpers.entity_registry.EntityRegistry.async_load" ) as mock_load: results = await asyncio.gather( diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index 9c7ddb09f85..b0f58f76a66 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -2,6 +2,7 @@ # pylint: disable=protected-access import asyncio from datetime import datetime, timedelta +from unittest.mock import patch from astral import Astral import jinja2 @@ -39,7 +40,6 @@ from homeassistant.helpers.template import Template from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import async_fire_time_changed DEFAULT_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE diff --git a/tests/helpers/test_frame.py b/tests/helpers/test_frame.py index 6daae51403c..7fc46b3699d 100644 --- a/tests/helpers/test_frame.py +++ b/tests/helpers/test_frame.py @@ -1,10 +1,10 @@ """Test the frame helper.""" +from unittest.mock import Mock, patch + import pytest from homeassistant.helpers import frame -from tests.async_mock import Mock, patch - async def test_extract_frame_integration(caplog): """Test extracting the current frame from integration context.""" diff --git a/tests/helpers/test_httpx_client.py b/tests/helpers/test_httpx_client.py index 5444cd4643d..53a6985f5cc 100644 --- a/tests/helpers/test_httpx_client.py +++ b/tests/helpers/test_httpx_client.py @@ -1,13 +1,13 @@ """Test the httpx client helper.""" +from unittest.mock import Mock, patch + import httpx import pytest from homeassistant.core import EVENT_HOMEASSISTANT_CLOSE import homeassistant.helpers.httpx_client as client -from tests.async_mock import Mock, patch - async def test_get_async_client_with_ssl(hass): """Test init async client with ssl.""" diff --git a/tests/helpers/test_instance_id.py b/tests/helpers/test_instance_id.py index 36e87b31a43..c8417c519a1 100644 --- a/tests/helpers/test_instance_id.py +++ b/tests/helpers/test_instance_id.py @@ -1,5 +1,5 @@ """Tests for instance ID helper.""" -from tests.async_mock import patch +from unittest.mock import patch async def test_get_id_empty(hass, hass_storage): diff --git a/tests/helpers/test_integration_platform.py b/tests/helpers/test_integration_platform.py index 6f0e56d34c8..d6c844c0d91 100644 --- a/tests/helpers/test_integration_platform.py +++ b/tests/helpers/test_integration_platform.py @@ -1,7 +1,8 @@ """Test integration platform helpers.""" +from unittest.mock import Mock + from homeassistant.setup import ATTR_COMPONENT, EVENT_COMPONENT_LOADED -from tests.async_mock import Mock from tests.common import mock_platform diff --git a/tests/helpers/test_intent.py b/tests/helpers/test_intent.py index bbb6e394ee0..e328d30ab7a 100644 --- a/tests/helpers/test_intent.py +++ b/tests/helpers/test_intent.py @@ -38,3 +38,21 @@ def test_async_validate_slots(): handler1.async_validate_slots( {"name": {"value": "kitchen"}, "probability": {"value": "0.5"}} ) + + +def test_fuzzy_match(): + """Test _fuzzymatch.""" + state1 = State("light.living_room_northwest", "off") + state2 = State("light.living_room_north", "off") + state3 = State("light.living_room_northeast", "off") + state4 = State("light.living_room_west", "off") + state5 = State("light.living_room", "off") + states = [state1, state2, state3, state4, state5] + + state = intent._fuzzymatch("Living Room", states, lambda state: state.name) + assert state == state5 + + state = intent._fuzzymatch( + "Living Room Northwest", states, lambda state: state.name + ) + assert state == state1 diff --git a/tests/helpers/test_network.py b/tests/helpers/test_network.py index 3330c42d9fc..06158558d5e 100644 --- a/tests/helpers/test_network.py +++ b/tests/helpers/test_network.py @@ -1,4 +1,6 @@ """Test network helper.""" +from unittest.mock import Mock, patch + import pytest from homeassistant.components import cloud @@ -15,7 +17,6 @@ from homeassistant.helpers.network import ( is_internal_request, ) -from tests.async_mock import Mock, patch from tests.common import mock_component diff --git a/tests/helpers/test_reload.py b/tests/helpers/test_reload.py index 3ed8d17b3f4..b4f47fa65b1 100644 --- a/tests/helpers/test_reload.py +++ b/tests/helpers/test_reload.py @@ -1,6 +1,7 @@ """Tests for the reload helper.""" import logging from os import path +from unittest.mock import AsyncMock, Mock, patch import pytest @@ -16,7 +17,6 @@ from homeassistant.helpers.reload import ( ) from homeassistant.loader import async_get_integration -from tests.async_mock import AsyncMock, Mock, patch from tests.common import ( MockModule, MockPlatform, diff --git a/tests/helpers/test_restore_state.py b/tests/helpers/test_restore_state.py index 15eed1c7e19..1a2fb2f57b5 100644 --- a/tests/helpers/test_restore_state.py +++ b/tests/helpers/test_restore_state.py @@ -1,5 +1,6 @@ """The tests for the Restore component.""" from datetime import datetime +from unittest.mock import patch from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.core import CoreState, State @@ -14,8 +15,6 @@ from homeassistant.helpers.restore_state import ( ) from homeassistant.util import dt as dt_util -from tests.async_mock import patch - async def test_caching_data(hass): """Test that we cache data.""" diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 72f4b06d91c..65d8b442bf0 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -6,6 +6,7 @@ from datetime import timedelta import logging from types import MappingProxyType from unittest import mock +from unittest.mock import patch from async_timeout import timeout import pytest @@ -20,7 +21,6 @@ from homeassistant.helpers import config_validation as cv, script from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import ( async_capture_events, async_fire_time_changed, @@ -824,9 +824,11 @@ async def test_wait_template_with_utcnow(hass): start_time = dt_util.utcnow().replace(minute=1) + timedelta(hours=48) try: - hass.async_create_task(script_obj.async_run(context=Context())) - await asyncio.wait_for(wait_started_flag.wait(), 1) - assert script_obj.is_running + non_maching_time = start_time.replace(hour=3) + with patch("homeassistant.util.dt.utcnow", return_value=non_maching_time): + hass.async_create_task(script_obj.async_run(context=Context())) + await asyncio.wait_for(wait_started_flag.wait(), 1) + assert script_obj.is_running match_time = start_time.replace(hour=12) with patch("homeassistant.util.dt.utcnow", return_value=match_time): @@ -848,13 +850,17 @@ async def test_wait_template_with_utcnow_no_match(hass): timed_out = False try: - hass.async_create_task(script_obj.async_run(context=Context())) - await asyncio.wait_for(wait_started_flag.wait(), 1) - assert script_obj.is_running - non_maching_time = start_time.replace(hour=3) with patch("homeassistant.util.dt.utcnow", return_value=non_maching_time): - async_fire_time_changed(hass, non_maching_time) + hass.async_create_task(script_obj.async_run(context=Context())) + await asyncio.wait_for(wait_started_flag.wait(), 1) + assert script_obj.is_running + + second_non_maching_time = start_time.replace(hour=4) + with patch( + "homeassistant.util.dt.utcnow", return_value=second_non_maching_time + ): + async_fire_time_changed(hass, second_non_maching_time) with timeout(0.1): await hass.async_block_till_done() @@ -955,7 +961,33 @@ async def test_wait_for_trigger_bad(hass, caplog): hass.async_create_task(script_obj.async_run()) await hass.async_block_till_done() + assert "Unknown error while setting up trigger" in caplog.text + + +async def test_wait_for_trigger_generated_exception(hass, caplog): + """Test bad wait_for_trigger.""" + script_obj = script.Script( + hass, + cv.SCRIPT_SCHEMA( + {"wait_for_trigger": {"platform": "state", "entity_id": "sensor.abc"}} + ), + "Test Name", + "test_domain", + ) + + async def async_attach_trigger_mock(*args, **kwargs): + raise ValueError("something bad") + + with mock.patch( + "homeassistant.components.homeassistant.triggers.state.async_attach_trigger", + wraps=async_attach_trigger_mock, + ): + hass.async_create_task(script_obj.async_run()) + await hass.async_block_till_done() + assert "Error setting up trigger" in caplog.text + assert "ValueError" in caplog.text + assert "something bad" in caplog.text async def test_condition_basic(hass): diff --git a/tests/helpers/test_selector.py b/tests/helpers/test_selector.py index 86ee6078e87..2916d616703 100644 --- a/tests/helpers/test_selector.py +++ b/tests/helpers/test_selector.py @@ -169,3 +169,21 @@ def test_target_selector_schema(schema): def test_action_selector_schema(schema): """Test action sequence selector.""" selector.validate_selector({"action": schema}) + + +@pytest.mark.parametrize( + "schema", + ({},), +) +def test_object_selector_schema(schema): + """Test object selector.""" + selector.validate_selector({"object": schema}) + + +@pytest.mark.parametrize( + "schema", + ({}, {"multiline": True}, {"multiline": False}), +) +def test_text_selector_schema(schema): + """Test text selector.""" + selector.validate_selector({"text": schema}) diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index a75593ddd40..95ccdc84395 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -2,6 +2,7 @@ from collections import OrderedDict from copy import deepcopy import unittest +from unittest.mock import AsyncMock, Mock, patch import pytest import voluptuous as vol @@ -26,7 +27,6 @@ from homeassistant.helpers import ( import homeassistant.helpers.config_validation as cv from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock, Mock, patch from tests.common import ( MockEntity, get_test_home_assistant, diff --git a/tests/helpers/test_significant_change.py b/tests/helpers/test_significant_change.py new file mode 100644 index 00000000000..e72951d36dd --- /dev/null +++ b/tests/helpers/test_significant_change.py @@ -0,0 +1,50 @@ +"""Test significant change helper.""" +import pytest + +from homeassistant.components.sensor import DEVICE_CLASS_BATTERY +from homeassistant.const import ATTR_DEVICE_CLASS, STATE_UNAVAILABLE, STATE_UNKNOWN +from homeassistant.core import State +from homeassistant.helpers import significant_change +from homeassistant.setup import async_setup_component + + +@pytest.fixture(name="checker") +async def checker_fixture(hass): + """Checker fixture.""" + checker = await significant_change.create_checker(hass, "test") + + def async_check_significant_change( + _hass, old_state, _old_attrs, new_state, _new_attrs, **kwargs + ): + return abs(float(old_state) - float(new_state)) > 4 + + hass.data[significant_change.DATA_FUNCTIONS][ + "test_domain" + ] = async_check_significant_change + return checker + + +async def test_signicant_change(hass, checker): + """Test initialize helper works.""" + assert await async_setup_component(hass, "sensor", {}) + + ent_id = "test_domain.test_entity" + attrs = {ATTR_DEVICE_CLASS: DEVICE_CLASS_BATTERY} + + assert checker.async_is_significant_change(State(ent_id, "100", attrs)) + + # Same state is not significant. + assert not checker.async_is_significant_change(State(ent_id, "100", attrs)) + + # State under 5 difference is not significant. (per test mock) + assert not checker.async_is_significant_change(State(ent_id, "96", attrs)) + + # Make sure we always compare against last significant change + assert checker.async_is_significant_change(State(ent_id, "95", attrs)) + + # State turned unknown + assert checker.async_is_significant_change(State(ent_id, STATE_UNKNOWN, attrs)) + + # State turned unavailable + assert checker.async_is_significant_change(State(ent_id, "100", attrs)) + assert checker.async_is_significant_change(State(ent_id, STATE_UNAVAILABLE, attrs)) diff --git a/tests/helpers/test_singleton.py b/tests/helpers/test_singleton.py index 03230a02ab8..c695efd94a8 100644 --- a/tests/helpers/test_singleton.py +++ b/tests/helpers/test_singleton.py @@ -1,10 +1,10 @@ """Test singleton helper.""" +from unittest.mock import Mock + import pytest from homeassistant.helpers import singleton -from tests.async_mock import Mock - @pytest.fixture def mock_hass(): diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py index 626f8a83744..89b0f3c6850 100644 --- a/tests/helpers/test_state.py +++ b/tests/helpers/test_state.py @@ -1,6 +1,7 @@ """Test state helpers.""" import asyncio from datetime import timedelta +from unittest.mock import patch import pytest @@ -21,7 +22,6 @@ import homeassistant.core as ha from homeassistant.helpers import state from homeassistant.util import dt as dt_util -from tests.async_mock import patch from tests.common import async_mock_service diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index 7fa6dd61845..61bf9fa8d0e 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -2,6 +2,7 @@ import asyncio from datetime import timedelta import json +from unittest.mock import Mock, patch import pytest @@ -13,7 +14,6 @@ from homeassistant.core import CoreState from homeassistant.helpers import storage from homeassistant.util import dt -from tests.async_mock import Mock, patch from tests.common import async_fire_time_changed MOCK_VERSION = 1 diff --git a/tests/helpers/test_storage_remove.py b/tests/helpers/test_storage_remove.py index 9a447771ea6..aa118aded59 100644 --- a/tests/helpers/test_storage_remove.py +++ b/tests/helpers/test_storage_remove.py @@ -2,11 +2,11 @@ import asyncio from datetime import timedelta import os +from unittest.mock import patch from homeassistant.helpers import storage from homeassistant.util import dt -from tests.async_mock import patch from tests.common import async_fire_time_changed, async_test_home_assistant diff --git a/tests/helpers/test_sun.py b/tests/helpers/test_sun.py index a877c7cdb00..b8ecd1ed86a 100644 --- a/tests/helpers/test_sun.py +++ b/tests/helpers/test_sun.py @@ -1,13 +1,12 @@ """The tests for the Sun helpers.""" # pylint: disable=protected-access from datetime import datetime, timedelta +from unittest.mock import patch from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET import homeassistant.helpers.sun as sun import homeassistant.util.dt as dt_util -from tests.async_mock import patch - def test_next_events(hass): """Test retrieving next sun events.""" diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index c8a8bc0710c..174d61ea470 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -2,6 +2,7 @@ from datetime import datetime import math import random +from unittest.mock import patch import pytest import pytz @@ -23,8 +24,6 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import UnitSystem -from tests.async_mock import patch - def _set_up_units(hass): """Set up the tests.""" diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index e8c5c756d59..8f555914682 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -2,6 +2,7 @@ import asyncio from os import path import pathlib +from unittest.mock import Mock, patch import pytest @@ -10,8 +11,6 @@ from homeassistant.helpers import translation from homeassistant.loader import async_get_integration from homeassistant.setup import async_setup_component, setup_component -from tests.async_mock import Mock, patch - @pytest.fixture def mock_config_flows(): diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index 90567456bfb..e9754f83a26 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -2,16 +2,18 @@ import asyncio from datetime import timedelta import logging +from unittest.mock import AsyncMock, Mock, patch import urllib.error import aiohttp import pytest import requests +from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.core import CoreState from homeassistant.helpers import update_coordinator from homeassistant.util.dt import utcnow -from tests.async_mock import AsyncMock, Mock, patch from tests.common import async_fire_time_changed _LOGGER = logging.getLogger(__name__) @@ -284,3 +286,27 @@ async def test_async_set_updated_data(crd): crd.async_set_updated_data(300) # We have created a new refresh listener assert crd._unsub_refresh is not old_refresh + + +async def test_stop_refresh_on_ha_stop(hass, crd): + """Test no update interval refresh when Home Assistant is stopping.""" + # Add subscriber + update_callback = Mock() + crd.async_add_listener(update_callback) + + update_interval = crd.update_interval + + # Test we update with subscriber + async_fire_time_changed(hass, utcnow() + update_interval) + await hass.async_block_till_done() + assert crd.data == 1 + + # Fire Home Assistant stop event + hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + hass.state = CoreState.stopping + await hass.async_block_till_done() + + # Make sure no update with subscriber after stop event + async_fire_time_changed(hass, utcnow() + update_interval) + await hass.async_block_till_done() + assert crd.data == 1 diff --git a/tests/mock/zwave.py b/tests/mock/zwave.py index 41c0aa5727b..5565b43a78e 100644 --- a/tests/mock/zwave.py +++ b/tests/mock/zwave.py @@ -1,7 +1,7 @@ """Mock helpers for Z-Wave component.""" -from pydispatch import dispatcher +from unittest.mock import MagicMock -from tests.async_mock import MagicMock +from pydispatch import dispatcher def value_changed(value): diff --git a/tests/scripts/test_auth.py b/tests/scripts/test_auth.py index c9ada99dc29..3ab19450879 100644 --- a/tests/scripts/test_auth.py +++ b/tests/scripts/test_auth.py @@ -1,10 +1,11 @@ """Test the auth script to manage local users.""" +from unittest.mock import Mock, patch + import pytest from homeassistant.auth.providers import homeassistant as hass_auth from homeassistant.scripts import auth as script_auth -from tests.async_mock import Mock, patch from tests.common import register_auth_provider diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 10034cb08af..6eaaee87af0 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -1,12 +1,12 @@ """Test check_config script.""" import logging +from unittest.mock import patch import pytest from homeassistant.config import YAML_CONFIG_FILE import homeassistant.scripts.check_config as check_config -from tests.async_mock import patch from tests.common import get_test_config_dir, patch_yaml_files _LOGGER = logging.getLogger(__name__) diff --git a/tests/scripts/test_init.py b/tests/scripts/test_init.py index 2c14bfdcf0a..8feef2d3384 100644 --- a/tests/scripts/test_init.py +++ b/tests/scripts/test_init.py @@ -1,7 +1,7 @@ """Test script init.""" -import homeassistant.scripts as scripts +from unittest.mock import patch -from tests.async_mock import patch +import homeassistant.scripts as scripts @patch("homeassistant.scripts.get_default_config_dir", return_value="/default") diff --git a/tests/test_bootstrap.py b/tests/test_bootstrap.py index ed31724b58b..fc653c25d0b 100644 --- a/tests/test_bootstrap.py +++ b/tests/test_bootstrap.py @@ -2,7 +2,7 @@ # pylint: disable=protected-access import asyncio import os -from unittest.mock import Mock +from unittest.mock import Mock, patch import pytest @@ -11,7 +11,6 @@ import homeassistant.config as config_util from homeassistant.exceptions import HomeAssistantError import homeassistant.util.dt as dt_util -from tests.async_mock import patch from tests.common import ( MockModule, MockPlatform, diff --git a/tests/test_config.py b/tests/test_config.py index 931b672d01b..7dd7d61e8ef 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -4,6 +4,7 @@ from collections import OrderedDict import copy import os from unittest import mock +from unittest.mock import AsyncMock, Mock, patch import pytest import voluptuous as vol @@ -34,7 +35,6 @@ from homeassistant.loader import async_get_integration from homeassistant.util import dt as dt_util from homeassistant.util.yaml import SECRET_YAML -from tests.async_mock import AsyncMock, Mock, patch from tests.common import get_test_config_dir, patch_yaml_files CONFIG_DIR = get_test_config_dir() diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 0be89af4810..24444f6a6c0 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1,6 +1,7 @@ """Test the config manager.""" import asyncio from datetime import timedelta +from unittest.mock import AsyncMock, patch import pytest @@ -10,7 +11,6 @@ from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.setup import async_setup_component from homeassistant.util import dt -from tests.async_mock import AsyncMock, patch from tests.common import ( MockConfigEntry, MockEntity, @@ -792,7 +792,7 @@ async def test_update_entry_options_and_trigger_listener(hass, manager): async def test_setup_raise_not_ready(hass, caplog): """Test a setup raising not ready.""" - entry = MockConfigEntry(domain="test") + entry = MockConfigEntry(title="test_title", domain="test") mock_setup_entry = AsyncMock(side_effect=ConfigEntryNotReady) mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry)) @@ -802,7 +802,7 @@ async def test_setup_raise_not_ready(hass, caplog): await entry.async_setup(hass) assert len(mock_call.mock_calls) == 1 - assert "Config entry for test not ready yet" in caplog.text + assert "Config entry 'test_title' for test integration not ready yet" in caplog.text p_hass, p_wait_time, p_setup = mock_call.mock_calls[0][1] assert p_hass is hass @@ -1523,7 +1523,7 @@ async def test_unique_id_ignore(hass, manager): result2 = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_IGNORE}, - data={"unique_id": "mock-unique-id"}, + data={"unique_id": "mock-unique-id", "title": "Ignored Title"}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -1537,6 +1537,7 @@ async def test_unique_id_ignore(hass, manager): assert entry.source == "ignore" assert entry.unique_id == "mock-unique-id" + assert entry.title == "Ignored Title" async def test_manual_add_overrides_ignored_entry(hass, manager): @@ -1605,7 +1606,7 @@ async def test_unignore_step_form(hass, manager): result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_IGNORE}, - data={"unique_id": "mock-unique-id"}, + data={"unique_id": "mock-unique-id", "title": "Ignored Title"}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -1613,6 +1614,7 @@ async def test_unignore_step_form(hass, manager): assert entry.source == "ignore" assert entry.unique_id == "mock-unique-id" assert entry.domain == "comp" + assert entry.title == "Ignored Title" await manager.async_remove(entry.entry_id) @@ -1649,7 +1651,7 @@ async def test_unignore_create_entry(hass, manager): result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_IGNORE}, - data={"unique_id": "mock-unique-id"}, + data={"unique_id": "mock-unique-id", "title": "Ignored Title"}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -1657,6 +1659,7 @@ async def test_unignore_create_entry(hass, manager): assert entry.source == "ignore" assert entry.unique_id == "mock-unique-id" assert entry.domain == "comp" + assert entry.title == "Ignored Title" await manager.async_remove(entry.entry_id) @@ -1690,7 +1693,7 @@ async def test_unignore_default_impl(hass, manager): result = await manager.flow.async_init( "comp", context={"source": config_entries.SOURCE_IGNORE}, - data={"unique_id": "mock-unique-id"}, + data={"unique_id": "mock-unique-id", "title": "Ignored Title"}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -1698,6 +1701,7 @@ async def test_unignore_default_impl(hass, manager): assert entry.source == "ignore" assert entry.unique_id == "mock-unique-id" assert entry.domain == "comp" + assert entry.title == "Ignored Title" await manager.async_remove(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/test_core.py b/tests/test_core.py index 541f75e6343..0bf00d92c45 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -6,6 +6,7 @@ import functools import logging import os from tempfile import TemporaryDirectory +from unittest.mock import MagicMock, Mock, PropertyMock, patch import pytest import pytz @@ -40,7 +41,6 @@ from homeassistant.exceptions import ( import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM -from tests.async_mock import MagicMock, Mock, PropertyMock, patch from tests.common import async_capture_events, async_mock_service PST = pytz.timezone("America/Los_Angeles") @@ -169,6 +169,30 @@ async def test_stage_shutdown(hass): assert len(test_all) == 2 +async def test_shutdown_calls_block_till_done_after_shutdown_run_callback_threadsafe( + hass, +): + """Ensure shutdown_run_callback_threadsafe is called before the final async_block_till_done.""" + stop_calls = [] + + async def _record_block_till_done(): + nonlocal stop_calls + stop_calls.append("async_block_till_done") + + def _record_shutdown_run_callback_threadsafe(loop): + nonlocal stop_calls + stop_calls.append(("shutdown_run_callback_threadsafe", loop)) + + with patch.object(hass, "async_block_till_done", _record_block_till_done), patch( + "homeassistant.core.shutdown_run_callback_threadsafe", + _record_shutdown_run_callback_threadsafe, + ): + await hass.async_stop() + + assert stop_calls[-2] == ("shutdown_run_callback_threadsafe", hass.loop) + assert stop_calls[-1] == "async_block_till_done" + + async def test_pending_sheduler(hass): """Add a coro to pending tasks.""" call_count = [] diff --git a/tests/test_loader.py b/tests/test_loader.py index c05240893de..c1c27f56cb7 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1,11 +1,12 @@ """Test to verify that we can load components.""" +from unittest.mock import ANY, patch + import pytest from homeassistant import core, loader from homeassistant.components import http, hue from homeassistant.components.hue import light as hue_light -from tests.async_mock import ANY, patch from tests.common import MockModule, async_mock_service, mock_integration @@ -97,7 +98,7 @@ async def test_helpers_wrapper(hass): async def test_custom_component_name(hass): - """Test the name attribte of custom components.""" + """Test the name attribute of custom components.""" integration = await loader.async_get_integration(hass, "test_standalone") int_comp = integration.get_component() assert int_comp.__name__ == "custom_components.test_standalone" @@ -171,6 +172,11 @@ def test_integration_properties(hass): "requirements": ["test-req==1.0.0"], "zeroconf": ["_hue._tcp.local."], "homekit": {"models": ["BSB002"]}, + "dhcp": [ + {"hostname": "tesla_*", "macaddress": "4CFCAA*"}, + {"hostname": "tesla_*", "macaddress": "044EAF*"}, + {"hostname": "tesla_*", "macaddress": "98ED5C*"}, + ], "ssdp": [ { "manufacturer": "Royal Philips Electronics", @@ -189,6 +195,11 @@ def test_integration_properties(hass): assert integration.domain == "hue" assert integration.homekit == {"models": ["BSB002"]} assert integration.zeroconf == ["_hue._tcp.local."] + assert integration.dhcp == [ + {"hostname": "tesla_*", "macaddress": "4CFCAA*"}, + {"hostname": "tesla_*", "macaddress": "044EAF*"}, + {"hostname": "tesla_*", "macaddress": "98ED5C*"}, + ] assert integration.ssdp == [ { "manufacturer": "Royal Philips Electronics", @@ -219,6 +230,7 @@ def test_integration_properties(hass): assert integration.is_built_in is False assert integration.homekit is None assert integration.zeroconf is None + assert integration.dhcp is None assert integration.ssdp is None assert integration.mqtt is None @@ -237,6 +249,7 @@ def test_integration_properties(hass): assert integration.is_built_in is False assert integration.homekit is None assert integration.zeroconf == [{"type": "_hue._tcp.local.", "name": "hue*"}] + assert integration.dhcp is None assert integration.ssdp is None @@ -294,6 +307,30 @@ def _get_test_integration_with_zeroconf_matcher(hass, name, config_flow): ) +def _get_test_integration_with_dhcp_matcher(hass, name, config_flow): + """Return a generated test integration with a dhcp matcher.""" + return loader.Integration( + hass, + f"homeassistant.components.{name}", + None, + { + "name": name, + "domain": name, + "config_flow": config_flow, + "dependencies": [], + "requirements": [], + "zeroconf": [], + "dhcp": [ + {"hostname": "tesla_*", "macaddress": "4CFCAA*"}, + {"hostname": "tesla_*", "macaddress": "044EAF*"}, + {"hostname": "tesla_*", "macaddress": "98ED5C*"}, + ], + "homekit": {"models": [name]}, + "ssdp": [{"manufacturer": name, "modelName": name}], + }, + ) + + async def test_get_custom_components(hass, enable_custom_integrations): """Verify that custom components are cached.""" test_1_integration = _get_test_integration(hass, "test_1", False) @@ -346,6 +383,23 @@ async def test_get_zeroconf(hass): ] +async def test_get_dhcp(hass): + """Verify that custom components with dhcp are found.""" + test_1_integration = _get_test_integration_with_dhcp_matcher(hass, "test_1", True) + + with patch("homeassistant.loader.async_get_custom_components") as mock_get: + mock_get.return_value = { + "test_1": test_1_integration, + } + dhcp = await loader.async_get_dhcp(hass) + dhcp_for_domain = [entry for entry in dhcp if entry["domain"] == "test_1"] + assert dhcp_for_domain == [ + {"domain": "test_1", "hostname": "tesla_*", "macaddress": "4CFCAA*"}, + {"domain": "test_1", "hostname": "tesla_*", "macaddress": "044EAF*"}, + {"domain": "test_1", "hostname": "tesla_*", "macaddress": "98ED5C*"}, + ] + + async def test_get_homekit(hass): """Verify that custom components with homekit are found.""" test_1_integration = _get_test_integration(hass, "test_1", True) diff --git a/tests/test_main.py b/tests/test_main.py index 40c34b77b50..5ec6460301f 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,9 +1,9 @@ """Test methods in __main__.""" +from unittest.mock import PropertyMock, patch + from homeassistant import __main__ as main from homeassistant.const import REQUIRED_PYTHON_VER -from tests.async_mock import PropertyMock, patch - @patch("sys.exit") def test_validate_python(mock_exit): diff --git a/tests/test_requirements.py b/tests/test_requirements.py index c0a1f0723ac..5f74e504de8 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -1,5 +1,6 @@ """Test requirements module.""" import os +from unittest.mock import call, patch import pytest @@ -11,7 +12,6 @@ from homeassistant.requirements import ( async_process_requirements, ) -from tests.async_mock import call, patch from tests.common import MockModule, mock_integration @@ -244,3 +244,26 @@ async def test_discovery_requirements_zeroconf(hass, partial_manifest): assert len(mock_process.mock_calls) == 2 # zeroconf also depends on http assert mock_process.mock_calls[0][1][2] == zeroconf.requirements + + +async def test_discovery_requirements_dhcp(hass): + """Test that we load dhcp discovery requirements.""" + hass.config.skip_pip = False + dhcp = await loader.async_get_integration(hass, "dhcp") + + mock_integration( + hass, + MockModule( + "comp", + partial_manifest={ + "dhcp": [{"hostname": "somfy_*", "macaddress": "B8B7F1*"}] + }, + ), + ) + with patch( + "homeassistant.requirements.async_process_requirements", + ) as mock_process: + await async_get_integration_with_requirements(hass, "comp") + + assert len(mock_process.mock_calls) == 1 # dhcp does not depend on http + assert mock_process.mock_calls[0][1][2] == dhcp.requirements diff --git a/tests/test_setup.py b/tests/test_setup.py index ebcb1093779..539ed3f1442 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -3,6 +3,7 @@ import asyncio import os import threading +from unittest.mock import Mock, patch import pytest import voluptuous as vol @@ -18,7 +19,6 @@ from homeassistant.helpers.config_validation import ( ) import homeassistant.util.dt as dt_util -from tests.async_mock import Mock, patch from tests.common import ( MockConfigEntry, MockModule, diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index fde1cb6ca2e..5219212f1cf 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -245,9 +245,9 @@ class AiohttpClientMockResponse: """Return mock response.""" return self.response - async def text(self, encoding="utf-8"): + async def text(self, encoding="utf-8", errors="strict"): """Return mock response as a string.""" - return self.response.decode(encoding) + return self.response.decode(encoding, errors=errors) async def json(self, encoding="utf-8", content_type=None): """Return mock response as a json.""" @@ -276,7 +276,7 @@ def mock_aiohttp_client(): """Context manager to mock aiohttp client.""" mocker = AiohttpClientMocker() - def create_session(hass, *args): + def create_session(hass, *args, **kwargs): session = mocker.create_session(hass.loop) async def close_session(event): diff --git a/tests/testing_config/custom_components/test/device_tracker.py b/tests/testing_config/custom_components/test/device_tracker.py index 59f163d6714..e4853d156ce 100644 --- a/tests/testing_config/custom_components/test/device_tracker.py +++ b/tests/testing_config/custom_components/test/device_tracker.py @@ -16,6 +16,9 @@ class MockScannerEntity(ScannerEntity): def __init__(self): """Init.""" self.connected = False + self._hostname = "test.hostname.org" + self._ip_address = "0.0.0.0" + self._mac_address = "ad:de:ef:be:ed:fe:" @property def source_type(self): @@ -30,6 +33,21 @@ class MockScannerEntity(ScannerEntity): """ return 100 + @property + def ip_address(self) -> str: + """Return the primary ip address of the device.""" + return self._ip_address + + @property + def mac_address(self) -> str: + """Return the mac address of the device.""" + return self._mac_address + + @property + def hostname(self) -> str: + """Return hostname of the device.""" + return self._hostname + @property def is_connected(self): """Return true if the device is connected to the network.""" diff --git a/tests/util/test_async.py b/tests/util/test_async.py index 047697168b4..d4fdce1e912 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -1,13 +1,12 @@ """Tests for async util methods from Python source.""" import asyncio import time +from unittest.mock import MagicMock, Mock, patch import pytest from homeassistant.util import async_ as hasync -from tests.async_mock import MagicMock, Mock, patch - @patch("asyncio.coroutines.iscoroutine") @patch("concurrent.futures.Future") @@ -51,7 +50,8 @@ def test_fire_coroutine_threadsafe_from_inside_event_loop( def test_run_callback_threadsafe_from_inside_event_loop(mock_ident, _): """Testing calling run_callback_threadsafe from inside an event loop.""" callback = MagicMock() - loop = MagicMock() + + loop = Mock(spec=["call_soon_threadsafe"]) loop._thread_ident = None mock_ident.return_value = 5 @@ -169,3 +169,45 @@ async def test_gather_with_concurrency(): ) assert results == [2, 2, -1, -1] + + +async def test_shutdown_run_callback_threadsafe(hass): + """Test we can shutdown run_callback_threadsafe.""" + hasync.shutdown_run_callback_threadsafe(hass.loop) + callback = MagicMock() + + with pytest.raises(RuntimeError): + hasync.run_callback_threadsafe(hass.loop, callback) + + +async def test_run_callback_threadsafe(hass): + """Test run_callback_threadsafe runs code in the event loop.""" + it_ran = False + + def callback(): + nonlocal it_ran + it_ran = True + + assert hasync.run_callback_threadsafe(hass.loop, callback) + assert it_ran is False + + # Verify that async_block_till_done will flush + # out the callback + await hass.async_block_till_done() + assert it_ran is True + + +async def test_callback_is_always_scheduled(hass): + """Test run_callback_threadsafe always calls call_soon_threadsafe before checking for shutdown.""" + # We have to check the shutdown state AFTER the callback is scheduled otherwise + # the function could continue on and the caller call `future.result()` after + # the point in the main thread where callbacks are no longer run. + + callback = MagicMock() + hasync.shutdown_run_callback_threadsafe(hass.loop) + + with patch.object(hass.loop, "call_soon_threadsafe") as mock_call_soon_threadsafe: + with pytest.raises(RuntimeError): + hasync.run_callback_threadsafe(hass.loop, callback) + + mock_call_soon_threadsafe.assert_called_once() diff --git a/tests/util/test_init.py b/tests/util/test_init.py index d3f45b968bc..34e95013b26 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -1,13 +1,12 @@ """Test Home Assistant util methods.""" from datetime import datetime, timedelta +from unittest.mock import MagicMock, patch import pytest from homeassistant import util import homeassistant.util.dt as dt_util -from tests.async_mock import MagicMock, patch - def test_sanitize_filename(): """Test sanitize_filename.""" @@ -25,6 +24,34 @@ def test_sanitize_path(): assert util.sanitize_path("~/../test/path") == "" +def test_raise_if_invalid_filename(): + """Test raise_if_invalid_filename.""" + assert util.raise_if_invalid_filename("test") is None + + with pytest.raises(ValueError): + util.raise_if_invalid_filename("/test") + + with pytest.raises(ValueError): + util.raise_if_invalid_filename("..test") + + with pytest.raises(ValueError): + util.raise_if_invalid_filename("\\test") + + with pytest.raises(ValueError): + util.raise_if_invalid_filename("\\../test") + + +def test_raise_if_invalid_path(): + """Test raise_if_invalid_path.""" + assert util.raise_if_invalid_path("test/path") is None + + with pytest.raises(ValueError): + assert util.raise_if_invalid_path("~test/path") + + with pytest.raises(ValueError): + assert util.raise_if_invalid_path("~/../test/path") + + def test_slugify(): """Test slugify.""" assert util.slugify("T-!@#$!#@$!$est") == "t_est" @@ -41,6 +68,12 @@ def test_slugify(): assert util.slugify("Tèst_äöüß_ÄÖÜ") == "test_aouss_aou" assert util.slugify("影師嗎") == "ying_shi_ma" assert util.slugify("けいふぉんと") == "keihuonto" + assert util.slugify("$") == "unknown" + assert util.slugify("Ⓐ") == "unknown" + assert util.slugify("ⓑ") == "unknown" + assert util.slugify("$$$") == "unknown" + assert util.slugify("$something") == "something" + assert util.slugify("") == "" def test_repr_helper(): diff --git a/tests/util/test_json.py b/tests/util/test_json.py index af858967150..1cbaaae7d23 100644 --- a/tests/util/test_json.py +++ b/tests/util/test_json.py @@ -7,6 +7,7 @@ import os import sys from tempfile import mkdtemp import unittest +from unittest.mock import Mock import pytest @@ -19,8 +20,6 @@ from homeassistant.util.json import ( save_json, ) -from tests.async_mock import Mock - # Test data that can be saved as JSON TEST_JSON_A = {"a": 1, "B": "two"} TEST_JSON_B = {"a": "one", "B": 2} diff --git a/tests/util/test_location.py b/tests/util/test_location.py index 403e24121ad..9eb2dc70561 100644 --- a/tests/util/test_location.py +++ b/tests/util/test_location.py @@ -1,10 +1,11 @@ """Test Home Assistant location util methods.""" +from unittest.mock import Mock, patch + import aiohttp import pytest import homeassistant.util.location as location_util -from tests.async_mock import Mock, patch from tests.common import load_fixture # Paris diff --git a/tests/util/test_logging.py b/tests/util/test_logging.py index 04d6f133381..1a82c35e82d 100644 --- a/tests/util/test_logging.py +++ b/tests/util/test_logging.py @@ -2,13 +2,12 @@ import asyncio import logging import queue +from unittest.mock import patch import pytest import homeassistant.util.logging as logging_util -from tests.async_mock import patch - def test_sensitive_data_filter(): """Test the logging sensitive data filter.""" diff --git a/tests/util/test_package.py b/tests/util/test_package.py index 064d1379e08..0c251662444 100644 --- a/tests/util/test_package.py +++ b/tests/util/test_package.py @@ -4,14 +4,13 @@ import logging import os from subprocess import PIPE import sys +from unittest.mock import MagicMock, call, patch import pkg_resources import pytest import homeassistant.util.package as package -from tests.async_mock import MagicMock, call, patch - RESOURCE_DIR = os.path.abspath( os.path.join(os.path.dirname(__file__), "..", "resources") ) diff --git a/tests/util/test_timeout.py b/tests/util/test_timeout.py index edd8f4107a4..39ec8d916bd 100644 --- a/tests/util/test_timeout.py +++ b/tests/util/test_timeout.py @@ -266,3 +266,60 @@ async def test_mix_zone_timeout_trigger_global_cool_down(): pass await asyncio.sleep(0.2) + + +async def test_simple_zone_timeout_freeze_without_timeout_cleanup(hass): + """Test a simple zone timeout freeze on a zone that does not have a timeout set.""" + timeout = TimeoutManager() + + async def background(): + async with timeout.async_freeze("test"): + await asyncio.sleep(0.4) + + async with timeout.async_timeout(0.1): + hass.async_create_task(background()) + await asyncio.sleep(0.2) + + +async def test_simple_zone_timeout_freeze_without_timeout_cleanup2(hass): + """Test a simple zone timeout freeze on a zone that does not have a timeout set.""" + timeout = TimeoutManager() + + async def background(): + async with timeout.async_freeze("test"): + await asyncio.sleep(0.2) + + with pytest.raises(asyncio.TimeoutError): + async with timeout.async_timeout(0.1): + hass.async_create_task(background()) + await asyncio.sleep(0.3) + + +async def test_simple_zone_timeout_freeze_without_timeout_exeption(): + """Test a simple zone timeout freeze on a zone that does not have a timeout set.""" + timeout = TimeoutManager() + + with pytest.raises(asyncio.TimeoutError): + async with timeout.async_timeout(0.1): + try: + async with timeout.async_freeze("test"): + raise RuntimeError() + except RuntimeError: + pass + + await asyncio.sleep(0.4) + + +async def test_simple_zone_timeout_zone_with_timeout_exeption(): + """Test a simple zone timeout freeze on a zone that does not have a timeout set.""" + timeout = TimeoutManager() + + with pytest.raises(asyncio.TimeoutError): + async with timeout.async_timeout(0.1): + try: + async with timeout.async_timeout(0.3, "test"): + raise RuntimeError() + except RuntimeError: + pass + + await asyncio.sleep(0.3) diff --git a/tests/util/yaml/test_init.py b/tests/util/yaml/test_init.py index 1c5b9bd9fd8..34097287bc3 100644 --- a/tests/util/yaml/test_init.py +++ b/tests/util/yaml/test_init.py @@ -3,6 +3,7 @@ import io import logging import os import unittest +from unittest.mock import patch import pytest @@ -11,7 +12,6 @@ from homeassistant.exceptions import HomeAssistantError import homeassistant.util.yaml as yaml from homeassistant.util.yaml import loader as yaml_loader -from tests.async_mock import patch from tests.common import get_test_config_dir, patch_yaml_files diff --git a/tox.ini b/tox.ini index cc1df307bfe..9c9963c28ee 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = py37, py38, lint, pylint, typing, cov +envlist = py38, py39, lint, pylint, typing, cov skip_missing_interpreters = True ignore_basepython_conflict = True